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#[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 Termux,
43 #[default]
45 Keyboard,
46 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); 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 '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 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#[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
325macro_rules! help_msg_basic {
326 () => {
327 r#" __
328 ____ / ] ________ __
329 / __ \_ _ ___ ___ ___ __ _| |_ | ___ \__ __ ___ _| |__
330_/ / \/ | |X _/ __/ __\ / \ / | |___| | | | / __/_ __/
331\ X | | | | |__ | __X | X || | | X_ __/ |_| X__ | | X
332 \ \__/\ __X_| \___/___/ \___/| | | | \ \_ X__ /___ / \__/
333 \____/ |/ |__\ \__/
334
335 Made by DM Earth in 2024-2025.
336
337 Command line arguments:
338
339-W width
340 Map width (default is 21)
341
342-H height
343 Map height (default is 21)
344
345-S [rhombus|rect|hex]
346 Map shape (rectangle is default). Max number of countries N=4 for rhombus and rectangle, and N=6 for the hexagon.
347
348-l [2|3| ... N]
349 Sets L, the number of countries (default is N).
350
351-i [0|1|2|3|4]
352 Inequality between the countries (0 is the lowest, 4 in the highest).
353
354-q [1|2| ... L]
355 Choose player's location by its quality (1 = the best available on the map, L = the worst). Only in the singleplayer mode.
356
357-r
358 Absolutely random initial conditions, overrides options -l, -i, and -q.
359
360-d [ee|e|n|h|hh]
361 Difficulty level (AI) from the easiest to the hardest (default is normal).
362
363-s [p|sss|ss|s|n|f|ff|fff]
364 Game speed from the slowest to the fastest (default is normal).
365
366-R seed
367 Specify a random seed (unsigned integer) for map generation.
368
369-T
370 Show the timeline.
371
372-E [1|2| ... L]
373 Start a server for not more than L clients.
374
375-e port
376 Server's port (19140 is default).
377
378-C IP
379 Start a client and connect to the provided server's IP-address.
380
381-c port
382 Clients's port (19150 is default).
383
384-m [keyboard|termux|hybrid]
385 Control method.
386
387-v
388 Display the version number
389
390-h
391 Display this help"#
392 };
393}
394
395#[cfg(feature = "net-proto")]
396macro_rules! help_msg_net_proto {
397 () => {
398 r#"
399
400-p [tcp|udp|ws|websocket]
401 Specify the networking protocol to use in multiplayer (udp is default)."#
402 };
403}
404
405#[cfg(not(feature = "net-proto"))]
406macro_rules! help_msg_net_proto {
407 () => {
408 ""
409 };
410}
411
412pub const HELP_MSG: &str = concat![help_msg_basic!(), help_msg_net_proto!()];