Skip to main content

curseofrust_cli_parser/
lib.rs

1use std::{cmp::max, ffi::OsStr, net::SocketAddr};
2
3use curseofrust::state::{BasicOpts, MultiplayerOpts};
4
5use wrapper::{DifficultyWrapper as Difficulty, SpeedWrapper as Speed, StencilWrapper as Stencil};
6
7mod wrapper;
8
9const DEFAULT_SERVER_PORT: u16 = 19140;
10const DEFAULT_CLIENT_PORT: u16 = 19150;
11
12/// Parses the command line arguments.
13#[deprecated(note = "use `parse_to_options` instead")]
14pub fn parse(
15    args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
16) -> Result<(BasicOpts, MultiplayerOpts), Error> {
17    let options = parse_to_options(args)?;
18    if options.exit {
19        std::process::exit(0);
20    }
21    Ok((options.basic, options.multiplayer))
22}
23
24#[cfg(feature = "net-proto")]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26#[non_exhaustive]
27pub enum Protocol {
28    Tcp,
29    #[default]
30    Udp,
31    WebSocket,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35#[non_exhaustive]
36pub enum ControlMode {
37    /// Control mode designed for termux.
38    Termux,
39    /// Pure-keyboard controlling. Same as curseofwar.
40    #[default]
41    Keyboard,
42    /// Basic cursor controlling and full-featured
43    /// keyboard control.
44    Hybrid,
45}
46
47#[cfg(feature = "net-proto")]
48impl std::str::FromStr for Protocol {
49    type Err = Error;
50
51    fn from_str(s: &str) -> Result<Self, Self::Err> {
52        Ok(match s {
53            "tcp" => Protocol::Tcp,
54            "udp" => Protocol::Udp,
55            "ws" | "websocket" => Protocol::WebSocket,
56            _ => {
57                return Err(Error::UnknownVariant {
58                    ty: "protocol",
59                    variants: &["tcp", "udp", "ws or websocket"],
60                    value: s.to_owned(),
61                })
62            }
63        })
64    }
65}
66
67impl std::str::FromStr for ControlMode {
68    type Err = Error;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        Ok(match s {
72            "termux" => Self::Termux,
73            "keyboard" => Self::Keyboard,
74            "hybrid" => Self::Hybrid,
75            _ => {
76                return Err(Error::UnknownVariant {
77                    ty: "control_mode",
78                    variants: &["termux", "keyboard", "hybrid"],
79                    value: s.to_owned(),
80                })
81            }
82        })
83    }
84}
85
86pub fn parse_to_options<I, S>(args: I) -> Result<Options, Error>
87where
88    I: IntoIterator<Item = S>,
89    S: Into<std::ffi::OsString>,
90{
91    let mut basic_opts = BasicOpts::default();
92    let mut multiplayer_opts = MultiplayerOpts::default();
93    let mut exit = false;
94    let mut cm = ControlMode::default();
95
96    #[cfg(feature = "net-proto")]
97    let mut protocol = Protocol::default();
98
99    let args = clap_lex::RawArgs::new(args);
100    let mut cursor = args.cursor();
101    args.next(&mut cursor); // skip bin
102    let mut short_buf = String::new();
103    while let Some(arg) = args.next(&mut cursor) {
104        if let Some(mut s) = arg.to_short() {
105            while let Some(Ok(flag)) = s.next() {
106                macro_rules! parse {
107                    ($a:expr, $t:expr, $vt:ty) => {{
108                        short_buf.clear();
109                        short_buf.extend(s.by_ref().map_while(Result::ok));
110                        let v: Result<$vt, _> = (!short_buf.is_empty())
111                            .then_some(&*short_buf)
112                            .or_else(|| args.next(&mut cursor).and_then(|a| a.to_value().ok()))
113                            .ok_or_else(|| Error::MissingValue { arg: $a, ty: $t })
114                            .and_then(|a| a.parse().map_err(From::from));
115                        v
116                    }};
117                    ($a:expr, $t:expr) => {
118                        parse!($a, $t, _)
119                    };
120                }
121                match flag {
122                    'W' => basic_opts.width = parse!("-W", "integer")?,
123                    // Minimum height.
124                    'H' => basic_opts.height = max(parse!("-H", "integer")?, 5),
125                    'S' => basic_opts.shape = parse!("-S", "shape", Stencil)?.0,
126                    'l' => basic_opts.locations = parse!("-l", "integer")?,
127                    'i' => basic_opts.inequality = Some(parse!("-i", "integer")?),
128                    'q' => basic_opts.conditions = Some(parse!("-q", "integer")?),
129                    'r' => basic_opts.keep_random = true,
130                    'd' => basic_opts.difficulty = parse!("-d", "difficulty", Difficulty)?.0,
131                    's' => basic_opts.speed = parse!("-s", "speed", Speed)?.0,
132                    'R' => basic_opts.seed = parse!("-R", "integer")?,
133                    'T' => basic_opts.timeline = true,
134                    'E' => {
135                        basic_opts.clients = parse!("-E", "integer")?;
136                        if matches!(multiplayer_opts, MultiplayerOpts::None) {
137                            multiplayer_opts = MultiplayerOpts::Server {
138                                port: DEFAULT_SERVER_PORT,
139                            };
140                        }
141                    }
142                    'e' => {
143                        multiplayer_opts = MultiplayerOpts::Server {
144                            port: parse!("-e", "integer")?,
145                        };
146                    }
147                    'C' => {
148                        let parsed = parse!("-C", "SocketAddr")?;
149                        if let MultiplayerOpts::Client { ref mut server, .. } = multiplayer_opts {
150                            *server = parsed;
151                        } else {
152                            multiplayer_opts = MultiplayerOpts::Client {
153                                server: parsed,
154                                port: DEFAULT_CLIENT_PORT,
155                            }
156                        }
157                    }
158                    'c' => {
159                        let parsed = parse!("-c", "integer")?;
160                        if let MultiplayerOpts::Client { ref mut port, .. } = multiplayer_opts {
161                            *port = parsed
162                        } else {
163                            multiplayer_opts = MultiplayerOpts::Client {
164                                server: SocketAddr::from((
165                                    std::net::Ipv4Addr::LOCALHOST,
166                                    DEFAULT_SERVER_PORT,
167                                )),
168                                port: parsed,
169                            };
170                        }
171                    }
172                    'v' => {
173                        println!("curseofrust");
174                        exit = true
175                    }
176                    'h' => {
177                        println!("{HELP_MSG}");
178                        exit = true
179                    }
180
181                    #[cfg(feature = "net-proto")]
182                    'p' => protocol = parse!("-p", "protocol", Protocol)?,
183
184                    'm' => cm = parse!("-m", "control mode", ControlMode)?,
185
186                    f => return Err(Error::UnknownFlag { flag: f }),
187                }
188            }
189        }
190    }
191
192    // Fix a weird bug.
193    if basic_opts.shape == curseofrust::grid::Stencil::Rect {
194        basic_opts.width += 10;
195    }
196
197    Ok(Options {
198        basic: basic_opts,
199        multiplayer: multiplayer_opts,
200        exit,
201
202        #[cfg(feature = "net-proto")]
203        protocol,
204        control_mode: cm,
205    })
206}
207
208/// The options for the program.
209#[derive(Debug)]
210#[non_exhaustive]
211pub struct Options {
212    pub basic: BasicOpts,
213    pub multiplayer: MultiplayerOpts,
214    pub exit: bool,
215    pub control_mode: ControlMode,
216
217    #[cfg(feature = "net-proto")]
218    pub protocol: Protocol,
219}
220
221#[derive(Debug)]
222#[non_exhaustive]
223pub enum Error {
224    MissingValue {
225        arg: &'static str,
226        ty: &'static str,
227    },
228    InvalidIntValueFmt(std::num::ParseIntError),
229    InvalidIpAddrValueFmt(std::net::AddrParseError),
230    NonUnicodeValue {
231        content: Box<OsStr>,
232    },
233    UnknownFlag {
234        flag: char,
235    },
236    UnknownVariant {
237        ty: &'static str,
238        variants: &'static [&'static str],
239        value: String,
240    },
241}
242
243impl std::fmt::Display for Error {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            Error::MissingValue { arg, ty } => {
247                write!(
248                    f,
249                    "missing value for argument '{arg}', expected type '{ty}'"
250                )
251            }
252            Error::InvalidIntValueFmt(err) => write!(f, "invalid integer formatting: {err}"),
253            Error::InvalidIpAddrValueFmt(err) => write!(f, "invalid IP-address formatting: {err}"),
254            Error::NonUnicodeValue { content } => {
255                write!(f, "non-unicode value: {content:?}")
256            }
257            Error::UnknownFlag { flag } => write!(f, "unknown flag: {flag}"),
258            Error::UnknownVariant {
259                ty,
260                variants,
261                value,
262            } => write!(
263                f,
264                "unknown variant '{value}' for type '{ty}', expected one of: {variants:?}",
265            ),
266        }
267    }
268}
269
270impl<'a> From<&'a OsStr> for Error {
271    #[inline]
272    fn from(value: &'a OsStr) -> Self {
273        Error::NonUnicodeValue {
274            content: value.into(),
275        }
276    }
277}
278
279impl From<std::num::ParseIntError> for Error {
280    #[inline]
281    fn from(value: std::num::ParseIntError) -> Self {
282        Error::InvalidIntValueFmt(value)
283    }
284}
285
286impl From<std::net::AddrParseError> for Error {
287    #[inline]
288    fn from(value: std::net::AddrParseError) -> Self {
289        Error::InvalidIpAddrValueFmt(value)
290    }
291}
292
293impl std::error::Error for Error {}
294
295/// The help message for the program.
296pub const HELP_MSG: &str = r#"                                __
297   ____                        /  ]  ________             __
298  / __ \_ _ ___ ___ ___    __ _| |_  |  ___  \__  __ ___ _| |__
299_/ /  \/ | |X _/ __/ __\  /   \   /  | |___| | | |  / __/_  __/
300\ X    | | | | |__ | __X  | X || |   | X_  __/ |_|  X__ | | X
301 \ \__/\ __X_| \___/___/  \___/| |   | | \ \_ X__ /___ /  \__/
302  \____/                       |/    |_\  \__/
303
304  Made by DM Earth in 2024-2025.
305
306  Command line arguments:
307
308-W width
309  Map width (default is 21)
310
311-H height
312  Map height (default is 21)
313
314-S [rhombus|rect|hex]
315  Map shape (rectangle is default). Max number of countries N=4 for rhombus and rectangle, and N=6 for the hexagon.
316
317-l [2|3| ... N]
318  Sets L, the number of countries (default is N).
319
320-i [0|1|2|3|4]
321  Inequality between the countries (0 is the lowest, 4 in the highest).
322
323-q [1|2| ... L]
324  Choose player's location by its quality (1 = the best available on the map, L = the worst). Only in the singleplayer mode.
325
326-r
327  Absolutely random initial conditions, overrides options -l, -i, and -q.
328
329-d [ee|e|n|h|hh]
330  Difficulty level (AI) from the easiest to the hardest (default is normal).
331
332-s [p|sss|ss|s|n|f|ff|fff]
333  Game speed from the slowest to the fastest (default is normal).
334
335-R seed
336  Specify a random seed (unsigned integer) for map generation.
337
338-T
339  Show the timeline.
340
341-E [1|2| ... L]
342  Start a server for not more than L clients.
343
344-e port
345  Server's port (19140 is default).
346
347-C IP
348  Start a client and connect to the provided server's IP-address.
349
350-c port
351  Clients's port (19150 is default).
352
353-m [keyboard|termux|hybrid]
354  Control method.
355
356-v
357  Display the version number
358
359-h
360  Display this help
361"#;