diff --git a/Cargo.lock b/Cargo.lock index 63f5b81..e999646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,10 +720,12 @@ name = "factorio-belt" version = "1.5.5" dependencies = [ "anyhow", + "base64", "charming", "clap", "csv", "dirs", + "flate2", "glob", "handlebars", "indicatif", @@ -737,6 +739,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" diff --git a/Cargo.toml b/Cargo.toml index ddd94ce..fe95373 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,5 @@ dirs = "^6" thiserror = "^2" charming = { version = "0.6.0", features = ["ssr"] } rand = "0.9.1" +base64 = "0.22.1" +flate2 = "1.0" diff --git a/blueprints/vanilla_mall.txt b/blueprints/vanilla_mall.txt new file mode 100644 index 0000000..d7b1a1b --- /dev/null +++ b/blueprints/vanilla_mall.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs new file mode 100644 index 0000000..793d1d1 --- /dev/null +++ b/src/blueprint/mod.rs @@ -0,0 +1,114 @@ +use std::path::PathBuf; + +use crate::{core::{BenchmarkError, Result}, util::blueprint_string::{parse_blueprint, save_blueprint_json, BlueprintString}}; + + +#[derive(Debug, Clone)] +pub struct BlueprintConfig { + pub string: Option, + pub file: PathBuf, + pub output: PathBuf, + pub recursive: bool, +} + +pub async fn run( + config: &BlueprintConfig +) -> Result<()> { + tracing::debug!("Running blueprint generation"); + + if config.string.is_some() { + process_string(config)?; + } + let recursive = config.recursive; + // Get the path provided by the blueprint config + // and process that path. + if config.file.is_file() { + return process_file(&config, &config.file); + } else if config.file.is_dir() { + return process_directory(&config.file, &config, recursive); + } else { + tracing::error!("Provided path is neither a file nor a directory: {:?}", config.file); + return Err(BenchmarkError::InvalidBlueprintPath { path: config.file.clone(), reason: "Provided path is neither a file nor a directory".into() }); + } +} + +fn process_string(config: &BlueprintConfig) -> Result<()> { + tracing::debug!("Using blueprint string: {:?}", config.string); + + let temp_file = config.file.with_file_name(".temp.txt"); + std::fs::write(&temp_file, config.string.as_ref().unwrap())?; + tracing::debug!("Temporary blueprint file created at: {:?}", temp_file); + + let result = process_file(&config, &temp_file); + + std::fs::remove_file(&temp_file)?; + tracing::debug!("Temporary blueprint file removed: {:?}", temp_file); + return result; +} + +fn create_save_file( + blueprint: &BlueprintString, + config: &BlueprintConfig, +) -> Result<()> { + let output_path = &config.output; + tracing::debug!("Creating save file at: {:?}", output_path); + + // Ensure the output directory exists + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent)?; + } + + save_blueprint_json(blueprint, output_path) + .map_err(|e| BenchmarkError::BlueprintEncode { reason: e.to_string() })?; + + tracing::debug!("Save file created successfully at: {:?}", output_path); + Ok(()) +} + + +fn process_file( + config: &BlueprintConfig, + file_path: &PathBuf, +) -> Result<()> { + tracing::debug!("Processing file: {:?}", file_path); + + if !file_path.exists() { + tracing::error!("File does not exist: {:?}", file_path); + return Err(BenchmarkError::InvalidBlueprintPath { path: file_path.clone(), reason: "File does not exist".into() }); + } + + let file_content = std::fs::read_to_string(file_path) + .map_err(|e| BenchmarkError::InvalidBlueprintString { path: file_path.clone(), reason: e.to_string() })?; + + // crate::util::blueprint_string::from_str(&file_content) + let blueprint = parse_blueprint(&file_content) + .map_err(|e| BenchmarkError::BlueprintDecode { path: file_path.clone(), reason: e.to_string() })?; + + // Depending on Blueprint this might be huge + tracing::debug!("Decoded file content: {:?}", blueprint); + + // We got the blueprint, as a json string. + // From here we should type/parse it to a blueprint structure, then create a save file. + create_save_file(&blueprint, &config)?; + + Ok(()) +} + +fn process_directory( + path: &PathBuf, + config: &BlueprintConfig, + recursive: bool, +) -> Result<()> { + tracing::debug!("Processing directory: {:?}", path); + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() && recursive { + tracing::debug!("Recursively processing directory: {:?}", path); + process_directory(&path, &config, recursive)?; + } else if path.is_file() { + process_file(&config, &path)?; + } + } + Ok(()) +} \ No newline at end of file diff --git a/src/core/error.rs b/src/core/error.rs index 72a9a4d..8f8d2fe 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -68,6 +68,18 @@ pub enum BenchmarkError { #[error("Invalid run order: {input}. Valid options: sequential, random, grouped")] InvalidRunOrder { input: String }, + + #[error("Invalid blueprint path: {path} - {reason}")] + InvalidBlueprintPath { path: PathBuf, reason: String }, + + #[error("Invalid blueprint string: {path} - {reason}")] + InvalidBlueprintString { path: PathBuf, reason: String }, + + #[error("Blueprint decoding error: {path} - {reason}")] + BlueprintDecode { path: PathBuf, reason: String }, + + #[error("Blueprint encoding error: {reason}")] + BlueprintEncode { reason: String }, } /// Get a hint for the FactorioProcessFailed error, if it exists diff --git a/src/main.rs b/src/main.rs index 93a6141..12515b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,9 @@ //! Parses CLI arguments, sets up logging, and dispatches to subcommands. mod benchmark; +mod blueprint; mod core; +mod util; use crate::core::Result; use clap::{Parser, Subcommand}; @@ -52,6 +54,19 @@ enum Commands { )] run_order: benchmark::RunOrder, }, + Blueprint { + #[arg(long, group = "blueprint_source")] + string: Option, + + #[arg(long, group = "blueprint_source", default_value = "blueprints")] + file: PathBuf, + + #[arg(long, default_value = "generated_saves/")] + output: PathBuf, + + #[arg(long, default_value = "false")] + recursive: bool, + }, } #[tokio::main] @@ -102,6 +117,16 @@ async fn main() -> Result<()> { benchmark::run(global_config, benchmark_config).await } + // Run the blueprint generation with a newly created blueprint config + Commands::Blueprint { string, file, output, recursive } => { + let blueprint_config = blueprint::BlueprintConfig { + string, + file, + output, + recursive, + }; + blueprint::run(&blueprint_config).await + } }; // If benchmark::run results in an error, print and exit diff --git a/src/util/blueprint_string.rs b/src/util/blueprint_string.rs new file mode 100644 index 0000000..9a97d48 --- /dev/null +++ b/src/util/blueprint_string.rs @@ -0,0 +1,395 @@ +// See https://wiki.factorio.com/Blueprint_string_format#Json_representation_of_a_blueprint/blueprint_book + +use base64::{prelude::BASE64_STANDARD, Engine}; +use flate2::read::ZlibDecoder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::path::Path; + +/// Parse a Factorio blueprint string and return the decoded JSON data. +/// +/// Blueprint strings follow this format: +/// 1. Version byte (currently 0) +/// 2. Base64 encoded zlib compressed JSON data +pub fn from_str(blueprint_string: &str) -> Result, Box> { + // Remove any whitespace and ensure we have at least one character + let trimmed = blueprint_string.trim(); + if trimmed.is_empty() { + return Err("Blueprint string is empty".into()); + } + + // Extract version byte and data + let version_byte = trimmed.chars().next().unwrap(); + if version_byte != '0' { + return Err(format!("Unsupported blueprint version: {}", version_byte).into()); + } + + // Get the base64 encoded data (skip the first character which is the version) + let base64_data = &trimmed[1..]; + + // Decode from base64 + let compressed_data = BASE64_STANDARD + .decode(base64_data) + .map_err(|e| format!("Failed to decode base64: {}", e))?; + + // Decompress using zlib + let mut decoder = ZlibDecoder::new(&compressed_data[..]); + let mut decompressed_data = Vec::new(); + match decoder.read_to_end(&mut decompressed_data) { + Ok(_) => Ok(decompressed_data), + Err(e) => { + // Try raw deflate if zlib fails + use flate2::read::DeflateDecoder; + let mut decoder = DeflateDecoder::new(&compressed_data[..]); + let mut decompressed_data = Vec::new(); + decoder + .read_to_end(&mut decompressed_data) + .map_err(|e2| format!("Failed to decompress both zlib and raw deflate: zlib={}, deflate={}", e, e2))?; + Ok(decompressed_data) + } + } +} + +/// Data structures representing Factorio blueprint format +/// Based on https://wiki.factorio.com/Blueprint_string_format + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlueprintString { + pub blueprint: Option, + pub blueprint_book: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Blueprint { + pub item: String, + pub label: Option, + pub label_color: Option, + pub entities: Option>, + pub tiles: Option>, + pub icons: Option>, + pub schedules: Option>, + pub description: Option, + #[serde(rename = "snap-to-grid")] + pub snap_to_grid: Option, + #[serde(rename = "absolute-snapping")] + pub absolute_snapping: Option, + #[serde(rename = "position-relative-to-grid")] + pub position_relative_to_grid: Option, + pub version: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlueprintBook { + pub item: String, + pub label: Option, + pub label_color: Option, + pub blueprints: Vec, + pub active_index: u32, + pub icons: Option>, + pub description: Option, + pub version: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlueprintEntry { + pub index: u32, + pub blueprint: Blueprint, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Entity { + pub entity_number: u32, + pub name: String, + pub position: Position, + pub direction: Option, + pub orientation: Option, + pub connections: Option>, + pub neighbours: Option>, + pub control_behavior: Option, // Complex nested object, using Value for flexibility + pub items: Option, // Can be HashMap or complex inventory items + pub recipe: Option, + pub bar: Option, + pub ammo_inventory: Option, + pub trunk_inventory: Option, + pub inventory: Option, + pub infinity_settings: Option, + #[serde(rename = "type")] + pub entity_type: Option, + pub input_priority: Option, + pub output_priority: Option, + pub filter: Option, + pub filters: Option>, + pub filter_mode: Option, + pub override_stack_size: Option, + pub drop_position: Option, + pub pickup_position: Option, + pub request_filters: Option, + pub request_from_buffers: Option, + pub parameters: Option, // Speaker parameters + pub alert_parameters: Option, // Speaker alert parameters + pub auto_launch: Option, + pub variation: Option, + pub color: Option, + pub station: Option, + pub manual_trains_limit: Option, + pub switch_state: Option, + pub tags: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Position { + pub x: f64, + pub y: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Color { + pub r: f64, + pub g: f64, + pub b: f64, + pub a: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Icon { + pub index: u32, + pub signal: SignalId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignalId { + pub name: String, + #[serde(rename = "type")] + pub signal_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tile { + pub name: String, + pub position: Position, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Connection { + #[serde(rename = "1")] + pub first: Option, + #[serde(rename = "2")] + pub second: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionPoint { + pub red: Option>, + pub green: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionData { + pub entity_id: u32, + pub circuit_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemFilter { + pub name: String, + pub index: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogisticFilter { + pub name: String, + pub index: u32, + pub count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Inventory { + pub filters: Option>, + pub bar: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InfinitySettings { + pub remove_unfiltered_items: bool, + pub filters: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InfinityFilter { + pub name: String, + pub count: u32, + pub mode: String, + pub index: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Schedule { + pub schedule: Vec, + pub locomotives: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScheduleRecord { + pub station: String, + pub wait_conditions: Vec, + pub temporary: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WaitCondition { + #[serde(rename = "type")] + pub condition_type: String, + pub compare_type: String, + pub ticks: Option, + pub condition: Option, // CircuitCondition object +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestFilters { + pub sections: Vec
, + #[serde(default)] + pub trash_not_requested: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Section { + pub index: u32, + pub filters: Option>, + pub multiplier: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Filter { + pub name: String, + pub quality: Option, + pub comparator: Option, +} + +/// Parse a blueprint string into a structured BlueprintString object +pub fn parse_blueprint(blueprint_string: &str) -> Result> { + let json_data = from_str(blueprint_string)?; + let json_str = String::from_utf8(json_data)?; + let blueprint: BlueprintString = serde_json::from_str(&json_str)?; + Ok(blueprint) +} + +/// Save the decompressed JSON from a blueprint string to a file +pub fn save_blueprint_json>(blueprint: &BlueprintString, file_path: P) -> Result<(), Box> { + let json_str = serde_json::to_string_pretty(blueprint)?; + let mut file = File::create(file_path)?; + file.write_all(json_str.as_bytes())?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_blueprint_string_decode() { + // Test basic decoding functionality with the vanilla mall blueprint + let vanilla_mall = include_str!("../../blueprints/vanilla_mall.txt"); + + // Test raw decoding first + let result = from_str(vanilla_mall.trim()); + assert!(result.is_ok(), "Failed to decode blueprint string: {:?}", result.err()); + + let json_data = result.unwrap(); + let json_str = String::from_utf8(json_data).unwrap(); + + // Verify we got a reasonable amount of JSON data + assert!(json_str.len() > 1000, "JSON data seems too small"); + + // Verify it's valid JSON + let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap(); + assert!(json_value.is_object(), "JSON should be an object"); + + // Test parsing with our BlueprintString struct + let blueprint = parse_blueprint(vanilla_mall.trim()).expect("Failed to parse blueprint"); + + // Verify the parsed structure + assert!(blueprint.blueprint.is_some(), "Should have a blueprint"); + assert!(blueprint.blueprint_book.is_none(), "Should not have a blueprint book"); + + let bp = blueprint.blueprint.unwrap(); + assert_eq!(bp.item, "blueprint", "Item should be 'blueprint'"); + assert!(bp.entities.is_some(), "Blueprint should have entities"); + assert!(bp.label.is_some(), "Blueprint should have a label"); + + let entities = bp.entities.unwrap(); + assert!(entities.len() > 100, "Should have many entities in this mall blueprint"); + + // Verify some entities have the complex structures we added support for + let has_request_filters = entities.iter().any(|e| e.request_filters.is_some()); + let has_filters = entities.iter().any(|e| e.filter.is_some()); + + assert!(has_request_filters, "Should have entities with request_filters"); + assert!(has_filters, "Should have entities with filters"); + } + + #[test] + fn test_complex_structures() { + // Test that our complex data structures (RequestFilters and Filter) serialize/deserialize correctly + let vanilla_mall = include_str!("../../blueprints/vanilla_mall.txt"); + let blueprint = parse_blueprint(vanilla_mall.trim()).expect("Failed to parse blueprint"); + + let bp = blueprint.blueprint.unwrap(); + let entities = bp.entities.unwrap(); + + // Find an entity with request_filters to test the structure + let entity_with_request_filters = entities.iter() + .find(|e| e.request_filters.is_some()) + .expect("Should find an entity with request_filters"); + + let request_filters = entity_with_request_filters.request_filters.as_ref().unwrap(); + assert!(!request_filters.sections.is_empty(), "Should have sections"); + + let first_section = &request_filters.sections[0]; + assert!(first_section.index > 0, "Section should have a valid index"); + + if let Some(filters) = &first_section.filters { + if !filters.is_empty() { + let first_filter = &filters[0]; + assert!(!first_filter.name.is_empty(), "Filter should have a name"); + } + } + + // Find an entity with a filter to test the structure + if let Some(entity_with_filter) = entities.iter().find(|e| e.filter.is_some()) { + let filter = entity_with_filter.filter.as_ref().unwrap(); + assert!(!filter.name.is_empty(), "Filter should have a name"); + } + } + + #[test] + fn test_save_blueprint_json() { + use std::fs; + use std::path::PathBuf; + + let vanilla_mall = include_str!("../../blueprints/vanilla_mall.txt"); + + // Test the save_blueprint_json function + let temp_file = PathBuf::from("test_output.json"); + + // Get the parsed blueprint + let blueprint = parse_blueprint(vanilla_mall.trim()) + .expect("Failed to parse blueprint for saving"); + // Save the JSON + let result = save_blueprint_json(&blueprint, &temp_file); + assert!(result.is_ok(), "Failed to save blueprint JSON: {:?}", result.err()); + + // Verify the file was created and contains valid JSON + assert!(temp_file.exists(), "Output file should exist"); + + let saved_content = fs::read_to_string(&temp_file).unwrap(); + let json_value: serde_json::Value = serde_json::from_str(&saved_content).unwrap(); + assert!(json_value.is_object(), "Saved content should be valid JSON"); + + // Clean up + let _ = fs::remove_file(&temp_file); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..415c062 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod blueprint_string;