diff --git a/bin/src/commands/utils.rs b/bin/src/commands/utils.rs index 42da02c7..e6316342 100644 --- a/bin/src/commands/utils.rs +++ b/bin/src/commands/utils.rs @@ -8,6 +8,7 @@ pub fn cli() -> Command { .about("Use HEMTT standalone utils") .subcommand_required(false) .arg_required_else_help(true) + .subcommand(utils::config::cli()) .subcommand(utils::inspect::cli()) .subcommand(utils::paa::cli()) .subcommand(utils::pbo::cli()) @@ -21,6 +22,7 @@ pub fn cli() -> Command { /// [`Error`] depending on the modules pub fn execute(matches: &ArgMatches) -> Result { match matches.subcommand() { + Some(("config", matches)) => utils::config::execute(matches), Some(("inspect", matches)) => utils::inspect::execute(matches), Some(("paa", matches)) => utils::paa::execute(matches), Some(("pbo", matches)) => utils::pbo::execute(matches), diff --git a/bin/src/utils/config/fmt.rs b/bin/src/utils/config/fmt.rs new file mode 100644 index 00000000..f436e19d --- /dev/null +++ b/bin/src/utils/config/fmt.rs @@ -0,0 +1,65 @@ +use std::{ + path::{Path, PathBuf}, + sync::{atomic::AtomicUsize, Arc}, +}; + +use clap::{ArgMatches, Command}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + +use crate::Error; + +#[must_use] +pub fn cli() -> Command { + Command::new("fmt").about("Format the config files").arg( + clap::Arg::new("path") + .help("Path to the config file or a folder to recursively fix") + .required(true), + ) +} + +/// Execute the convert command +/// +/// # Errors +/// [`Error`] depending on the modules +pub fn execute(matches: &ArgMatches) -> Result<(), Error> { + let path = PathBuf::from(matches.get_one::("path").expect("required")); + if path.is_dir() { + let count = Arc::new(AtomicUsize::new(0)); + let entries = walkdir::WalkDir::new(&path) + .into_iter() + .collect::, _>>()?; + entries + .par_iter() + .map(|entry| { + if entry.file_type().is_file() + && entry.path().extension().unwrap_or_default() == "cpp" + && entry.path().extension().unwrap_or_default() == "hpp" + && file(entry.path())? + { + count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + info!("Format `{}`", entry.path().display()); + } + Ok(()) + }) + .collect::, Error>>()?; + info!( + "Format {} files", + count.load(std::sync::atomic::Ordering::Relaxed) + ); + } else if file(&path)? { + info!("Format `{}`", path.display()); + } else { + info!("No changes in `{}`", path.display()); + } + Ok(()) +} + +fn file(path: &Path) -> Result { + // TODO do not release this lmao + let content = std::fs::read_to_string(path)?; + println!( + "{}", + hemtt_config::fmt::format(&content).expect("errors later") + ); + Ok(true) +} diff --git a/bin/src/utils/config/mod.rs b/bin/src/utils/config/mod.rs new file mode 100644 index 00000000..7da2038a --- /dev/null +++ b/bin/src/utils/config/mod.rs @@ -0,0 +1,28 @@ +mod fmt; + +use clap::{ArgMatches, Command}; + +use crate::Error; + +#[must_use] +pub fn cli() -> Command { + Command::new("config") + .about("Commands for Config files") + .arg_required_else_help(true) + .subcommand(fmt::cli()) +} + +/// Execute the paa command +/// +/// # Errors +/// [`Error`] depending on the modules +/// +/// # Panics +/// If the args are not present from clap +pub fn execute(matches: &ArgMatches) -> Result<(), Error> { + match matches.subcommand() { + Some(("fmt", matches)) => fmt::execute(matches), + + _ => unreachable!(), + } +} diff --git a/bin/src/utils/mod.rs b/bin/src/utils/mod.rs index ae81043a..550790f5 100644 --- a/bin/src/utils/mod.rs +++ b/bin/src/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod inspect; pub mod paa; pub mod pbo; diff --git a/libs/config/src/fmt/mod.rs b/libs/config/src/fmt/mod.rs new file mode 100644 index 00000000..5482455b --- /dev/null +++ b/libs/config/src/fmt/mod.rs @@ -0,0 +1,511 @@ +use chumsky::{prelude::*, Parser}; + +pub enum State { + Properties, + ClassName, + ClassExternal, +} + +pub fn format(content: &str) -> Result { + let config = config().parse(content).map_err(|e| { + e.into_iter() + .map(|e| e.to_string()) + .collect::>() + .join("\n") + })?; + Ok(config.write()) +} + +pub fn config() -> impl Parser> { + choice(( + property() + .padded() + .repeated() + .delimited_by(empty(), end()) + .map(Config), + end().padded().map(|()| Config(vec![])), + )) +} + +fn property() -> impl Parser> { + recursive(|rec| { + let class = just("class ") + .padded() + .ignore_then(ident().padded().labelled("class name")) + .then( + (just(':') + .padded() + .ignore_then(ident().padded().labelled("class parent"))) + .or_not(), + ) + .padded() + .then( + rec.labelled("class property") + .padded() + .repeated() + .padded() + .delimited_by(just('{'), just('}')) + .or_not(), + ) + .map(|((ident, parent), properties)| { + if let Some(properties) = properties { + Class { + name: ident, + parent, + external: false, + properties, + } + } else { + Class { + name: ident, + parent, + external: true, + properties: vec![], + } + } + }); + choice(( + choice(( + class.map(Property::Class), + choice(( + ident() + .padded() + .then_ignore(just('=').padded()) + .then( + value(";}\n".to_string()) + .or(none_of(";}") + .repeated() + .at_least(1) + .collect::() + .map(|s| s)) + .map(|s| vec![s]), + ) + .map(|(name, values)| Value { name, values }), + // an array of values + ident() + .padded() + .then_ignore(just("[]").padded()) + .then_ignore(just('=').padded()) + .then( + value(";}".to_string()) + .padded() + .separated_by(just(',').padded()) + .allow_trailing() + .delimited_by(just('{'), just('}')), + ) + .map(|(name, values)| Value::new(name, values)), + )) + .map(Property::Value), + )) + .then_ignore(one_of(";\n").padded().or_not()), + just("#").padded().ignore_then( + none_of("\n") + .repeated() + .at_least(1) + .collect::() + .map(|s| Property::Directive(format!("#{s}"))), + ), + )) + }) +} + +fn ident() -> impl Parser> { + one_of("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_()") + .repeated() + .at_least(1) + .collect::() + .map(|s| s) +} + +fn value(end: String) -> impl Parser> { + choice(( + string('"'), + none_of(end) + .repeated() + .at_least(1) + .collect::() + .map(|s| s), + )) + .padded() +} + +fn string(delimiter: char) -> impl Parser> { + let content = just(delimiter).not().or(just([delimiter; 2]).to(delimiter)); + let segment = just(delimiter) + .ignore_then(content.repeated()) + .then_ignore(just(delimiter)) + .collect(); + segment + .separated_by(just("\\n").padded()) + .at_least(1) + .collect::>() + .map(|s| { + format!( + "\"{}\"", + s.into_iter() + .collect::>() + .join("\n") + .replace("\\\n", "") + ) + }) +} + +#[derive(Debug, PartialEq)] +pub struct Config(Vec); + +impl Config { + #[must_use] + pub fn write(&self) -> String { + self.0 + .iter() + .map(|p| p.write(0)) + .collect::>() + .join("\n") + } +} + +#[derive(Debug, PartialEq)] +pub enum Property { + Class(Class), + Value(Value), + Directive(String), +} + +impl Property { + #[must_use] + pub fn write(&self, indent: u8) -> String { + match self { + Self::Class(c) => c.write(indent), + Self::Value(v) => v.write(indent), + Self::Directive(d) => d.clone(), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Class { + pub name: String, + pub external: bool, + pub parent: Option, + pub properties: Vec, +} + +impl Class { + pub fn write(&self, indent: u8) -> String { + let indent_str = " ".repeat(4 * indent as usize); + if self.external { + self.parent.as_ref().map_or_else( + || format!("{}class {};", indent_str, self.name), + |parent| format!("{}class {}: {} {{}};", indent_str, self.name, parent), + ) + } else { + let parent = self + .parent + .as_ref() + .map_or_else(String::new, |parent| format!(": {parent}")); + format!( + "{}class {}{} {{\n{}\n{}}};", + indent_str, + self.name, + parent, + self.properties + .iter() + .map(|p| p.write(indent + 1)) + .collect::>() + .join("\n"), + indent_str + ) + } + } +} + +#[derive(Debug, PartialEq, Eq)] +/// Either a single value or an array of values +/// +/// ```text +/// name = "Test" +/// names[] = { "Bob", "Alice" } +/// ``` +pub struct Value { + pub name: String, + pub values: Vec, +} + +impl Value { + #[must_use] + pub fn new(name: String, values: Vec) -> Self { + // see if the values are a string that contain multiple values + // "1, 2, 3" -> ["1", "2", "3"] + // respect parenthesis and strings + // "myFunc(1, 2), 3" -> ["myFunc(1, 2)", "3"] + let values = values + .into_iter() + .flat_map(|v| { + if v.contains(',') { + let mut values = vec![]; + let mut current = String::new(); + let mut in_string = false; + let mut in_parenthesis = 0; + for c in v.chars() { + match c { + '"' => in_string = !in_string, + '(' => in_parenthesis += 1, + ')' => in_parenthesis -= 1, + ',' if !in_string && in_parenthesis == 0 => { + values.push(current.trim().to_string()); + current.clear(); + } + _ => current.push(c), + } + } + values.push(current.trim().to_string()); + values + } else { + vec![v] + } + }) + .collect(); + Self { name, values } + } + + #[must_use] + pub fn write(&self, indent: u8) -> String { + let indent = " ".repeat(4 * indent as usize); + if self.values.len() == 1 { + format!("{}{} = {};", indent, self.name, self.values[0]) + } else { + format!( + "{}{}[] = {{{}}};", + indent, + self.name, + // use a single line if less than 60 characters + if self + .values + .iter() + .map(std::string::String::len) + .sum::() + + self.values.len() + < 60 + { + self.values.join(", ") + } else { + format!( + "\n{}\n{indent}", + self.values + .iter() + .map(|v| format!("{indent} {v}")) + .collect::>() + .join(",\n") + ) + } + ) + } + } +} + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn property_string() { + let input = "name = \"Test\""; + let result = property().parse(input).unwrap(); + assert_eq!( + result, + Property::Value(Value { + name: "name".to_string(), + values: vec!["Test".to_string()] + }) + ); + } + + #[test] + fn property_string_array() { + let input = "names[] = { \"Bob\", \"Alice\" }"; + let result = property().parse(input).unwrap(); + assert_eq!( + result, + Property::Value(Value { + name: "names".to_string(), + values: vec!["Bob".to_string(), "Alice".to_string()] + }) + ); + } + + #[test] + fn property_number_array() { + let input = "numbers[] = { 1, 2, 3 }"; + let result = property().parse(input).unwrap(); + assert_eq!( + result, + Property::Value(Value { + name: "numbers".to_string(), + values: vec!["1".to_string(), "2".to_string(), "3".to_string()] + }) + ); + } + + #[test] + fn property_garbage() { + let input = "name = just a bunch of garbage"; + let result = property().parse(input).unwrap(); + assert_eq!( + result, + Property::Value(Value { + name: "name".to_string(), + values: vec!["just a bunch of garbage".to_string()] + }) + ); + } + + #[test] + fn property_class() { + let input = "class Test: Parent { name = \"Test\"; }"; + let result = property().parse(input).unwrap(); + assert_eq!( + result, + Property::Class(Class { + name: "Test".to_string(), + external: false, + parent: Some("Parent".to_string()), + properties: vec![Property::Value(Value { + name: "name".to_string(), + values: vec!["Test".to_string()] + })] + }) + ); + } + + #[test] + fn multiple_properties() { + let input = r#"name = "Test"; names[] = { "Bob", "Alice" }"#; + let result = config().parse(input).unwrap(); + assert_eq!( + result, + Config(vec![ + Property::Value(Value { + name: "name".to_string(), + values: vec!["Test".to_string()] + }), + Property::Value(Value { + name: "names".to_string(), + values: vec!["Bob".to_string(), "Alice".to_string()] + }) + ]) + ); + } + + #[test] + fn multiple_classes() { + let input = r#"class Test: Parent { name = "Test"; }; class Test2 { name = "Test2"; };"#; + let result = config().parse(input).unwrap(); + assert_eq!( + result, + Config(vec![ + Property::Class(Class { + name: "Test".to_string(), + external: false, + parent: Some("Parent".to_string()), + properties: vec![Property::Value(Value { + name: "name".to_string(), + values: vec!["Test".to_string()] + })] + }), + Property::Class(Class { + name: "Test2".to_string(), + external: false, + parent: None, + properties: vec![Property::Value(Value { + name: "name".to_string(), + values: vec!["Test2".to_string()] + })] + }) + ]) + ); + } + + #[test] + fn nested_external() { + let input = r"class Something { class External; };"; + let result = config().parse(input).unwrap(); + assert_eq!( + result, + Config(vec![Property::Class(Class { + name: "Something".to_string(), + external: false, + parent: None, + properties: vec![Property::Class(Class { + name: "External".to_string(), + external: true, + parent: None, + properties: vec![] + })] + })]) + ); + } + + #[test] + fn ace_throwing_ammo() { + let input = r#"class CfgAmmo { + class Default; + class Grenade: Default { + GVAR(torqueDirection)[] = {1, 1, 0}; + GVAR(torqueMagnitude) = "(50 + random 100) * selectRandom [1, -1]"; + }; + class GrenadeCore: Default { + GVAR(torqueDirection)[] = {1, 1, 0}; + GVAR(torqueMagnitude) = "(50 + random 100) * selectRandom [1, -1]"; + }; +}; +"#; + let result = config().parse(input).unwrap(); + assert_eq!( + result, + Config(vec![Property::Class(Class { + name: "CfgAmmo".to_string(), + external: false, + parent: None, + properties: vec![ + Property::Class(Class { + name: "Default".to_string(), + external: false, + parent: None, + properties: vec![] + }), + Property::Class(Class { + name: "Grenade".to_string(), + external: false, + parent: Some("Default".to_string()), + properties: vec![ + Property::Value(Value { + name: "GVAR(torqueDirection)".to_string(), + values: vec!["1".to_string(), "1".to_string(), "0".to_string()] + }), + Property::Value(Value { + name: "GVAR(torqueMagnitude)".to_string(), + values: vec!["(50 + random 100) * selectRandom [1, -1]".to_string()] + }) + ] + }), + Property::Class(Class { + name: "GrenadeCore".to_string(), + external: false, + parent: Some("Default".to_string()), + properties: vec![ + Property::Value(Value { + name: "GVAR(torqueDirection)".to_string(), + values: vec!["1".to_string(), "1".to_string(), "0".to_string()] + }), + Property::Value(Value { + name: "GVAR(torqueMagnitude)".to_string(), + values: vec!["(50 + random 100) * selectRandom [1, -1]".to_string()] + }) + ] + }) + ] + })]) + ); + } +} diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs index 32bf1ee6..792237ad 100644 --- a/libs/config/src/lib.rs +++ b/libs/config/src/lib.rs @@ -7,10 +7,11 @@ use std::sync::Arc; mod analyze; +pub mod fmt; mod model; -pub use model::*; pub mod parse; pub mod rapify; +pub use model::*; pub use analyze::CONFIG_LINTS;