1use std::path::PathBuf;
2
3use pfx_core::cli;
4
5pub const DEFAULT_SIZE: (u32, u32) = (1920, 1080);
6
7const FLAGS: &[&str] = &[
8 "--seconds",
9 "--camera",
10 "--replay",
11 "--size",
12 "--stats",
13 "--seed",
14];
15
16#[derive(Clone, Debug, PartialEq)]
17pub struct Plan {
18 pub scene: PathBuf,
19 pub seconds: f64,
20 pub camera: Option<String>,
21 pub replay: Option<PathBuf>,
22 pub size: Option<(u32, u32)>,
23 pub stats: Option<PathBuf>,
24 pub seed: u64,
25}
26
27fn size(text: &str) -> Result<(u32, u32), String> {
28 let wrong = || cli::invalid(text, "--size", "expected WxH between 1 and 16384");
29 let (width, height) = text.split_once(['x', 'X']).ok_or_else(wrong)?;
30 let number = |part: &str| {
31 part.parse::<u32>()
32 .ok()
33 .filter(|value| (1..=16_384).contains(value))
34 .ok_or_else(wrong)
35 };
36 Ok((number(width)?, number(height)?))
37}
38
39pub fn parse(args: &[String]) -> Result<Plan, String> {
40 let mut scene = None;
41 let mut seconds = None;
42 let mut camera = None;
43 let mut replay = None;
44 let mut size_flag = None;
45 let mut stats = None;
46 let mut seed = 0;
47 let mut seen = Vec::new();
48 let mut words = args.iter();
49 while let Some(word) = words.next() {
50 let (flag, inline) = match word.split_once('=') {
51 Some((flag, value)) if flag.starts_with("--") => (flag, Some(value.to_string())),
52 _ => (word.as_str(), None),
53 };
54 if !flag.starts_with('-') {
55 if scene.is_some() {
56 return Err(cli::unexpected(word));
57 }
58 scene = Some(PathBuf::from(word));
59 continue;
60 }
61 if !FLAGS.contains(&flag) {
62 return Err(cli::unexpected(flag));
63 }
64 if seen.contains(&flag) {
65 return Err(cli::repeated(&cli::with_value(flag)));
66 }
67 seen.push(flag);
68 let value = match inline {
69 Some(value) => value,
70 None => words
71 .next()
72 .cloned()
73 .ok_or_else(|| cli::missing_value(flag))?,
74 };
75 match flag {
76 "--seconds" => {
77 let parsed = value
78 .parse::<f64>()
79 .ok()
80 .filter(|seconds| seconds.is_finite() && *seconds > 0.0)
81 .ok_or_else(|| cli::invalid(&value, flag, "expected a number above 0"))?;
82 seconds = Some(parsed);
83 }
84 "--camera" => camera = Some(value),
85 "--replay" => replay = Some(PathBuf::from(value)),
86 "--size" => size_flag = Some(size(&value)?),
87 "--stats" => stats = Some(PathBuf::from(value)),
88 _ => {
89 seed = value
90 .parse::<u64>()
91 .map_err(|error| cli::invalid(&value, flag, error))?;
92 }
93 }
94 }
95 let mut missing = Vec::new();
96 if seconds.is_none() {
97 missing.push(cli::with_value("--seconds"));
98 }
99 if scene.is_none() {
100 missing.push("<SCENE>".to_string());
101 }
102 if !missing.is_empty() {
103 return Err(cli::required(&missing));
104 }
105 Ok(Plan {
106 scene: scene.unwrap_or_default(),
107 seconds: seconds.unwrap_or_default(),
108 camera,
109 replay,
110 size: size_flag,
111 stats,
112 seed,
113 })
114}