diff --git a/schemsearch-cli/Cargo.toml b/schemsearch-cli/Cargo.toml index 471392e..ed78c86 100644 --- a/schemsearch-cli/Cargo.toml +++ b/schemsearch-cli/Cargo.toml @@ -16,6 +16,8 @@ futures = { version = "0.3", optional = true } sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls" , "mysql" ], optional = true } rayon = "1.7.0" indicatif = { version = "0.17.3", features = ["rayon"] } +serde = "1.0.157" +serde_json = "1.0.94" [features] sql = ["dep:schemsearch-sql", "dep:futures", "dep:sqlx"] diff --git a/schemsearch-cli/src/json_output.rs b/schemsearch-cli/src/json_output.rs new file mode 100644 index 0000000..79f963f --- /dev/null +++ b/schemsearch-cli/src/json_output.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use schemsearch_lib::SearchBehavior; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "event")] +pub enum JsonEvent { + Found(FoundEvent), + Init(InitEvent), + End(EndEvent), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FoundEvent { + pub name: String, + pub x: u16, + pub y: u16, + pub z: u16, + pub percent: f32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct InitEvent { + pub total: u32, + pub search_behavior: SearchBehavior, + pub start_time: u128, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct EndEvent { + pub end_time: u128, +} \ No newline at end of file diff --git a/schemsearch-cli/src/main.rs b/schemsearch-cli/src/main.rs index f76ae26..db89abf 100644 --- a/schemsearch-cli/src/main.rs +++ b/schemsearch-cli/src/main.rs @@ -16,13 +16,14 @@ */ mod types; +mod json_output; +mod sinks; use std::fmt::Debug; -use std::fs::File; -use std::io; -use std::io::{BufWriter, Write}; +use std::io::Write; use clap::{command, Arg, ArgAction, ValueHint}; use std::path::PathBuf; +use std::str::FromStr; use clap::error::ErrorKind; use schemsearch_lib::{search, SearchBehavior}; use crate::types::{PathSchematicSupplier, SchematicSupplierType}; @@ -38,6 +39,7 @@ use schemsearch_sql::load_all_schematics; use crate::types::SqlSchematicSupplier; use indicatif::{ParallelProgressIterator, ProgressStyle}; use schemsearch_files::Schematic; +use crate::sinks::{OutputFormat, OutputSink}; fn main() { #[allow(unused_mut)] @@ -92,20 +94,32 @@ fn main() { ) .arg( Arg::new("output") - .help("The output format") + .help("The output format and path [Format:Path] available formats: text, json, csv; available paths: std, (file path)") .short('o') .long("output") .action(ArgAction::Append) - .default_value("std") - .value_parser(["std_csv", "file_csv", "std", "file"]), - ) - .arg( - Arg::new("output-file") - .help("The output file") - .short('O') - .long("output-file") - .value_hint(ValueHint::FilePath) - .action(ArgAction::Append) + .default_value("text:std") + .value_parser(|s: &str| { + let mut split = s.splitn(2, ':'); + let format = match split.next() { + None => return Err("No format specified".to_string()), + Some(x) => x + }; + let path = match split.next() { + None => return Err("No path specified".to_string()), + Some(x) => x + }; + let format = match OutputFormat::from_str(format) { + Ok(x) => x, + Err(e) => return Err(e.to_string()), + }; + let path = match OutputSink::from_str(path) { + Ok(x) => x, + Err(e) => return Err(e.to_string()), + }; + + Ok((format, path)) + }), ) .arg( Arg::new("threshold") @@ -223,52 +237,26 @@ fn main() { cmd.error(ErrorKind::MissingRequiredArgument, "No schematics specified").exit(); } - let mut output_std = false; - let mut output_std_csv = false; - let mut output_file_csv = false; - let mut output_file = false; + let output: Vec<&(OutputFormat, OutputSink)> = matches.get_many::<(OutputFormat, OutputSink)>("output").expect("Error").collect(); + let mut output: Vec<(OutputFormat, Box)> = output.into_iter().map(|x| (x.0.clone(), x.1.output())).collect(); - for x in matches.get_many::("output").expect("Couldn't get output") { - match x.as_str() { - "std" => output_std = true, - "std_csv" => output_std_csv = true, - "file_csv" => output_file_csv = true, - "file" => output_file = true, - _ => {} - } - }; - let file: Option; - let mut file_out: Option> = None; - - if output_file || output_file_csv { - let output_file_path = match matches.get_one::("output-file") { - None => { - cmd.error(ErrorKind::MissingRequiredArgument, "No output file specified").exit(); - } - Some(x) => x - }; - - file = match File::create(output_file_path) { - Ok(x) => Some(x), - Err(e) => { - cmd.error(ErrorKind::Io, format!("Error while creating output file: {}", e.to_string())).exit(); - } - }; - file_out = Some(BufWriter::new(file.unwrap())); + for x in &mut output { + write!(x.1, "{}", x.0.start(schematics.len() as u32, &search_behavior, start.elapsed().as_millis())).unwrap(); } + ThreadPoolBuilder::new().num_threads(*matches.get_one::("threads").expect("Could not get threads")).build_global().unwrap(); - let matches: Vec = schematics.par_iter().progress_with_style(ProgressStyle::with_template("[{elapsed}, ETA: {eta}] {wide_bar} {pos}/{len} {per_sec}").unwrap()).map(|schem| { + let matches: Vec = schematics.par_iter().progress_with_style(ProgressStyle::with_template("[{elapsed}, ETA: {eta}] {wide_bar} {pos}/{len} {per_sec}").unwrap()).map(|schem| { match schem { SchematicSupplierType::PATH(schem) => { let schematic = match load_schem(&schem.path) { Some(x) => x, - None => return Result { + None => return SearchResult { name: schem.get_name(), matches: vec![] } }; - Result { + SearchResult { name: schem.get_name(), matches: search(schematic, &pattern, search_behavior) } @@ -277,7 +265,7 @@ fn main() { SchematicSupplierType::SQL(schem) => { match schem.get_schematic() { Ok(schematic) => { - Result { + SearchResult { name: schem.get_name(), matches: search(schematic, &pattern, search_behavior) } @@ -286,7 +274,7 @@ fn main() { if !output_std && !output_std_csv { println!("Error while loading schematic ({}): {}", schem.get_name(), e.to_string()); } - Result { + SearchResult { name: schem.get_name(), matches: vec![] } @@ -296,30 +284,21 @@ fn main() { } }).collect(); - let stdout = io::stdout(); - let mut lock = stdout.lock(); - for matching in matches { let schem_name = matching.name; let matching = matching.matches; for x in matching { - if output_std { - writeln!(lock, "Found match in '{}' at x: {}, y: {}, z: {}, % = {}", schem_name, x.0, x.1, x.2, x.3).unwrap(); - } - if output_std_csv { - writeln!(lock, "{},{},{},{},{}", schem_name, x.0, x.1, x.2, x.3).unwrap(); - } - if output_file { - writeln!(file_out.as_mut().unwrap(), "Found match in '{}' at x: {}, y: {}, z: {}, % = {}", schem_name, x.0, x.1, x.2, x.3).unwrap(); - } - if output_file_csv { - writeln!(file_out.as_mut().unwrap(), "{},{},{},{},{}", schem_name, x.0, x.1, x.2, x.3).unwrap(); + for out in &mut output { + write!(out.1, "{}", out.0.found_match(&schem_name, x)).unwrap(); } } } let end = std::time::Instant::now(); - println!("Finished in {:.2}s! Searched in {} Schematics", end.duration_since(start).as_secs_f32(), schematics.len()); + for x in &mut output { + write!(x.1, "{}", x.0.end(end.duration_since(start).as_millis())).unwrap(); + x.1.flush().unwrap(); + } } fn load_schem(schem_path: &PathBuf) -> Option { @@ -333,7 +312,7 @@ fn load_schem(schem_path: &PathBuf) -> Option { } #[derive(Debug, Clone)] -struct Result { +struct SearchResult { name: String, matches: Vec<(u16, u16, u16, f32)>, } diff --git a/schemsearch-cli/src/sinks.rs b/schemsearch-cli/src/sinks.rs new file mode 100644 index 0000000..c6a6478 --- /dev/null +++ b/schemsearch-cli/src/sinks.rs @@ -0,0 +1,88 @@ +use std::fs::File; +use std::io::BufWriter; +use std::str::FromStr; +use std::io::Write; +use schemsearch_lib::SearchBehavior; +use crate::json_output::{EndEvent, FoundEvent, InitEvent, JsonEvent}; + +#[derive(Debug, Clone)] +pub enum OutputSink { + Stdout, + File(String), +} + +#[derive(Debug, Clone)] +pub enum OutputFormat { + Text, + CSV, + JSON +} + +impl FromStr for OutputFormat { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "text" => Ok(OutputFormat::Text), + "csv" => Ok(OutputFormat::CSV), + "json" => Ok(OutputFormat::JSON), + _ => Err(format!("'{}' is not a valid output format", s)) + } + } +} + +impl FromStr for OutputSink { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "std" => Ok(OutputSink::Stdout), + _ => Ok(OutputSink::File(s.to_string())) + } + } +} + +impl OutputSink { + pub fn output(&self) -> Box { + match self { + OutputSink::Stdout => Box::new(std::io::stdout().lock()), + OutputSink::File(path) => Box::new(BufWriter::new(File::create(path).unwrap())) + } + } +} + +impl OutputFormat { + pub fn found_match(&self, name: &String, pos: (u16, u16, u16, f32)) -> String { + match self { + OutputFormat::Text => format!("Found match in '{}' at x: {}, y: {}, z: {}, % = {}\n", name, pos.0, pos.1, pos.2, pos.3), + OutputFormat::CSV => format!("{},{},{},{},{}\n", name, pos.0, pos.1, pos.2, pos.3), + OutputFormat::JSON => format!("{}\n", serde_json::to_string(&JsonEvent::Found(FoundEvent { + name: name.clone(), + x: pos.0, + y: pos.1, + z: pos.2, + percent: pos.3, + })).unwrap()) + } + } + + pub fn start(&self, total: u32, search_behavior: &SearchBehavior, start_time: u128) -> String { + match self { + OutputFormat::Text => format!("Starting search in {} schematics\n", total), + OutputFormat::CSV => format!("Name,X,Y,Z,Percent\n"), + OutputFormat::JSON => format!("{}\n", serde_json::to_string(&JsonEvent::Init(InitEvent { + total, + search_behavior: search_behavior.clone(), + start_time, + })).unwrap()) + } + } + + pub fn end(&self, end_time: u128) -> String { + match self { + OutputFormat::Text => format!("Search complete in {}s\n", end_time / 1000), + OutputFormat::CSV => format!("{}\n", end_time), + OutputFormat::JSON => format!("{}\n", serde_json::to_string(&JsonEvent::End(EndEvent{ end_time })).unwrap()) + } + } +} \ No newline at end of file diff --git a/schemsearch-lib/src/lib.rs b/schemsearch-lib/src/lib.rs index 4318366..68b5c6c 100644 --- a/schemsearch-lib/src/lib.rs +++ b/schemsearch-lib/src/lib.rs @@ -17,11 +17,12 @@ pub mod pattern_mapper; +use serde::{Deserialize, Serialize}; use pattern_mapper::match_palette; use schemsearch_files::Schematic; use crate::pattern_mapper::match_palette_adapt; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct SearchBehavior { pub ignore_block_data: bool, pub ignore_block_entities: bool, diff --git a/schemsearch-sql/src/lib.rs b/schemsearch-sql/src/lib.rs index 33e41b1..068059f 100644 --- a/schemsearch-sql/src/lib.rs +++ b/schemsearch-sql/src/lib.rs @@ -16,7 +16,7 @@ */ use std::sync::Mutex; -use sqlx::{ConnectOptions, Executor, MySql, MySqlPool, Pool, Row}; +use sqlx::{Executor, MySql, Pool, Row}; use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions}; use crate::filter::SchematicFilter;