Skip to main content

curseofrust_cli_parser/
lib.rs

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