Skip to main content

basalt_api/command/
args.rs

1//! Command argument types, parsing, and validation.
2//!
3//! Plugins declare command arguments with types, and the framework
4//! handles parsing, validation, error messages, DeclareCommands
5//! generation, and TabComplete responses.
6
7use std::collections::HashMap;
8
9/// Argument type for a command parameter.
10///
11/// Determines how the argument is parsed, validated, and presented
12/// in client-side tab-completion (Brigadier tree).
13#[derive(Debug, Clone)]
14pub enum Arg {
15    // --- Brigadier built-in parsers ---
16    /// Boolean (true/false). Parser ID 0.
17    Boolean,
18    /// 64-bit floating point number. Parser ID 2.
19    Double,
20    /// 64-bit integer. Parser ID 3.
21    Integer,
22    /// Free-form single-word string. Parser ID 5, mode SINGLE_WORD.
23    String,
24
25    // --- Minecraft-specific parsers ---
26    /// Entity selector (@a, @p, @r, @e, @s) or player name. Parser ID 6.
27    Entity,
28    /// Game profile (player name with tab-completion). Parser ID 7.
29    GameProfile,
30    /// Block position (integer coordinates, supports ~). Parser ID 42.
31    BlockPos,
32    /// Column position (x z integers). Parser ID 43.
33    ColumnPos,
34    /// 3D coordinates (supports ~ and ^). Parser ID 40.
35    Vec3,
36    /// 2D coordinates (x z, supports ~ and ^). Parser ID 41.
37    Vec2,
38    /// Block state (e.g., `stone`, `oak_planks[axis=x]`). Parser ID 44.
39    BlockState,
40    /// Item stack (e.g., `diamond_sword{Damage:10}`). Parser ID 46.
41    ItemStack,
42    /// Chat message (like GreedyString but with @mention support). Parser ID 48.
43    Message,
44    /// JSON text component. Parser ID 10.
45    Component,
46    /// Resource location (namespace:path). Parser ID 35.
47    ResourceLocation,
48    /// UUID. Parser ID 11.
49    Uuid,
50    /// Yaw and pitch rotation. Parser ID 39.
51    Rotation,
52
53    // --- Basalt extensions ---
54    /// Fixed set of choices with tab-completion. Uses string parser
55    /// with `minecraft:ask_server` suggestions.
56    Options(Vec<std::string::String>),
57    /// Player name — tab-completes with connected player names.
58    /// Uses `minecraft:game_profile` parser (ID 7).
59    Player,
60}
61
62/// Validation behavior for an argument.
63///
64/// Controls whether the framework validates the argument before
65/// calling the handler, and what error message is shown on failure.
66#[derive(Debug, Clone)]
67pub enum Validation {
68    /// Framework validates and sends a default error message.
69    Auto,
70    /// Framework validates and sends the custom error message.
71    Custom(std::string::String),
72    /// No validation — tab-completion still works, but the handler
73    /// receives the raw value and manages errors itself.
74    Disabled,
75}
76
77/// A declared command argument.
78#[derive(Debug, Clone)]
79pub struct CommandArg {
80    /// Argument name shown in the client UI and used as a key.
81    pub name: std::string::String,
82    /// The argument type (determines parsing and Brigadier node).
83    pub arg_type: Arg,
84    /// Validation behavior.
85    pub validation: Validation,
86    /// Whether this argument is required.
87    pub required: bool,
88}
89
90/// A parsed argument value.
91#[derive(Debug, Clone, PartialEq)]
92pub enum ArgValue {
93    /// A string value.
94    String(std::string::String),
95    /// A parsed integer.
96    Integer(i64),
97    /// A parsed double.
98    Double(f64),
99    /// A parsed boolean.
100    Boolean(bool),
101    /// Three f64 coordinates (x, y, z).
102    Vec3(f64, f64, f64),
103    /// Three i32 coordinates (x, y, z).
104    BlockPos(i32, i32, i32),
105}
106
107/// Parsed command arguments accessible by name.
108///
109/// Built by the framework after validating the raw argument string
110/// against the command's declared arguments.
111#[derive(Debug)]
112pub struct CommandArgs {
113    values: HashMap<std::string::String, ArgValue>,
114    raw: std::string::String,
115}
116
117impl CommandArgs {
118    /// Creates a new empty argument map.
119    pub fn new(raw: std::string::String) -> Self {
120        Self {
121            values: HashMap::new(),
122            raw,
123        }
124    }
125
126    /// Inserts a parsed value.
127    pub fn insert(&mut self, name: std::string::String, value: ArgValue) {
128        self.values.insert(name, value);
129    }
130
131    /// Gets a string argument by name.
132    pub fn get_string(&self, name: &str) -> Option<&str> {
133        match self.values.get(name) {
134            Some(ArgValue::String(s)) => Some(s),
135            _ => None,
136        }
137    }
138
139    /// Gets an integer argument by name.
140    pub fn get_integer(&self, name: &str) -> Option<i64> {
141        match self.values.get(name) {
142            Some(ArgValue::Integer(v)) => Some(*v),
143            _ => None,
144        }
145    }
146
147    /// Gets a double argument by name.
148    pub fn get_double(&self, name: &str) -> Option<f64> {
149        match self.values.get(name) {
150            Some(ArgValue::Double(v)) => Some(*v),
151            _ => None,
152        }
153    }
154
155    /// Gets a boolean argument by name.
156    pub fn get_bool(&self, name: &str) -> Option<bool> {
157        match self.values.get(name) {
158            Some(ArgValue::Boolean(v)) => Some(*v),
159            _ => None,
160        }
161    }
162
163    /// Gets a Vec3 argument by name (x, y, z as f64).
164    pub fn get_vec3(&self, name: &str) -> Option<(f64, f64, f64)> {
165        match self.values.get(name) {
166            Some(ArgValue::Vec3(x, y, z)) => Some((*x, *y, *z)),
167            _ => None,
168        }
169    }
170
171    /// Gets a BlockPos argument by name (x, y, z as i32).
172    pub fn get_block_pos(&self, name: &str) -> Option<(i32, i32, i32)> {
173        match self.values.get(name) {
174            Some(ArgValue::BlockPos(x, y, z)) => Some((*x, *y, *z)),
175            _ => None,
176        }
177    }
178
179    /// Returns the raw argument string before parsing.
180    pub fn raw(&self) -> &str {
181        &self.raw
182    }
183}
184
185impl Arg {
186    /// Returns how many tokens this argument type consumes.
187    ///
188    /// Vec3 and BlockPos consume 3 tokens, Vec2/ColumnPos/Rotation consume 2,
189    /// Message is greedy (0 means "consume all remaining"), everything else is 1.
190    pub fn token_count(&self) -> usize {
191        match self {
192            Arg::Vec3 | Arg::BlockPos => 3,
193            Arg::Vec2 | Arg::ColumnPos | Arg::Rotation => 2,
194            Arg::Message => 0, // greedy — consumes the rest
195            _ => 1,
196        }
197    }
198}
199
200/// Parses a raw argument string, trying variants if defined.
201///
202/// If `variants` is non-empty, tries each variant in order and
203/// returns the first successful parse. If all fail, returns the
204/// error from the last variant.
205pub fn parse_command_args(
206    raw: &str,
207    schema: &[CommandArg],
208    variants: &[Vec<CommandArg>],
209) -> Result<CommandArgs, std::string::String> {
210    if variants.is_empty() {
211        return parse_args(raw, schema);
212    }
213
214    // Sort variants by total token count descending — most specific
215    // (most tokens consumed) first. This ensures "10 64 -5" matches
216    // Vec3 before matching as a Player name.
217    let mut sorted: Vec<&Vec<CommandArg>> = variants.iter().collect();
218    sorted.sort_by(|a, b| {
219        let count_a: usize = a.iter().map(|arg| arg.arg_type.token_count()).sum();
220        let count_b: usize = b.iter().map(|arg| arg.arg_type.token_count()).sum();
221        count_b.cmp(&count_a)
222    });
223
224    let mut last_err = String::new();
225    for variant in sorted {
226        match parse_args(raw, variant) {
227            Ok(args) => return Ok(args),
228            Err(e) => last_err = e,
229        }
230    }
231    Err(last_err)
232}
233
234/// Parses a raw argument string against declared arguments.
235pub fn parse_args(raw: &str, schema: &[CommandArg]) -> Result<CommandArgs, std::string::String> {
236    let tokens: Vec<&str> = raw.split_whitespace().collect();
237    let mut args = CommandArgs::new(raw.to_string());
238
239    let required_count = schema.iter().filter(|a| a.required).count();
240    if tokens.len() < required_count {
241        let names: Vec<&str> = schema.iter().map(|a| a.name.as_str()).collect();
242        let usage = names
243            .iter()
244            .map(|n| format!("<{n}>"))
245            .collect::<Vec<_>>()
246            .join(" ");
247        return Err(format!("Usage: {usage}"));
248    }
249
250    let mut tok = 0; // current token position
251
252    for arg_def in schema {
253        // Message consumes everything from this position onward
254        if matches!(arg_def.arg_type, Arg::Message) {
255            let remainder: String = tokens[tok..].join(" ");
256            if remainder.is_empty() && arg_def.required {
257                return Err(format!("Missing required argument: {}", arg_def.name));
258            }
259            if !remainder.is_empty() {
260                args.insert(arg_def.name.clone(), ArgValue::String(remainder));
261            }
262            break;
263        }
264
265        let count = arg_def.arg_type.token_count();
266
267        if tok >= tokens.len() {
268            if arg_def.required {
269                return Err(format!("Missing required argument: {}", arg_def.name));
270            }
271            continue;
272        }
273
274        // Multi-token types: parse into typed values
275        if count > 1 {
276            if tok + count > tokens.len() {
277                if arg_def.required {
278                    return Err(format!(
279                        "Not enough values for '{}' (expected {count})",
280                        arg_def.name
281                    ));
282                }
283                continue;
284            }
285            let value = match &arg_def.arg_type {
286                Arg::Vec3 => {
287                    let x = tokens[tok]
288                        .parse::<f64>()
289                        .map_err(|_| format!("Invalid coordinate for '{}'", arg_def.name))?;
290                    let y = tokens[tok + 1]
291                        .parse::<f64>()
292                        .map_err(|_| format!("Invalid coordinate for '{}'", arg_def.name))?;
293                    let z = tokens[tok + 2]
294                        .parse::<f64>()
295                        .map_err(|_| format!("Invalid coordinate for '{}'", arg_def.name))?;
296                    ArgValue::Vec3(x, y, z)
297                }
298                Arg::BlockPos => {
299                    let x = tokens[tok]
300                        .parse::<i32>()
301                        .map_err(|_| format!("Invalid block coordinate for '{}'", arg_def.name))?;
302                    let y = tokens[tok + 1]
303                        .parse::<i32>()
304                        .map_err(|_| format!("Invalid block coordinate for '{}'", arg_def.name))?;
305                    let z = tokens[tok + 2]
306                        .parse::<i32>()
307                        .map_err(|_| format!("Invalid block coordinate for '{}'", arg_def.name))?;
308                    ArgValue::BlockPos(x, y, z)
309                }
310                _ => {
311                    // Vec2, ColumnPos, Rotation: store as joined string for now
312                    ArgValue::String(tokens[tok..tok + count].join(" "))
313                }
314            };
315            args.insert(arg_def.name.clone(), value);
316            tok += count;
317            continue;
318        }
319
320        let token = tokens[tok];
321        tok += 1;
322
323        if matches!(arg_def.validation, Validation::Disabled) {
324            args.insert(arg_def.name.clone(), ArgValue::String(token.to_string()));
325            continue;
326        }
327
328        match &arg_def.arg_type {
329            Arg::String
330            | Arg::Player
331            | Arg::Entity
332            | Arg::GameProfile
333            | Arg::BlockState
334            | Arg::ItemStack
335            | Arg::Component
336            | Arg::ResourceLocation
337            | Arg::Uuid => {
338                args.insert(arg_def.name.clone(), ArgValue::String(token.to_string()));
339            }
340            Arg::Integer => match token.parse::<i64>() {
341                Ok(v) => {
342                    args.insert(arg_def.name.clone(), ArgValue::Integer(v));
343                }
344                Err(_) => {
345                    return Err(match &arg_def.validation {
346                        Validation::Custom(msg) => msg.clone(),
347                        _ => format!("Expected an integer for '{}'", arg_def.name),
348                    });
349                }
350            },
351            Arg::Double => match token.parse::<f64>() {
352                Ok(v) => {
353                    args.insert(arg_def.name.clone(), ArgValue::Double(v));
354                }
355                Err(_) => {
356                    return Err(match &arg_def.validation {
357                        Validation::Custom(msg) => msg.clone(),
358                        _ => format!("Expected a number for '{}'", arg_def.name),
359                    });
360                }
361            },
362            Arg::Options(choices) => {
363                if choices.iter().any(|c| c == token) {
364                    args.insert(arg_def.name.clone(), ArgValue::String(token.to_string()));
365                } else {
366                    return Err(match &arg_def.validation {
367                        Validation::Custom(msg) => msg.clone(),
368                        _ => {
369                            let opts = choices.join(", ");
370                            format!("Invalid '{}'. Options: {opts}", arg_def.name)
371                        }
372                    });
373                }
374            }
375            Arg::Boolean => match token {
376                "true" => {
377                    args.insert(arg_def.name.clone(), ArgValue::Boolean(true));
378                }
379                "false" => {
380                    args.insert(arg_def.name.clone(), ArgValue::Boolean(false));
381                }
382                _ => {
383                    return Err(match &arg_def.validation {
384                        Validation::Custom(msg) => msg.clone(),
385                        _ => format!("Expected true/false for '{}'", arg_def.name),
386                    });
387                }
388            },
389            // Multi-token and greedy types handled above
390            Arg::Vec3
391            | Arg::Vec2
392            | Arg::BlockPos
393            | Arg::ColumnPos
394            | Arg::Rotation
395            | Arg::Message => {
396                unreachable!()
397            }
398        }
399    }
400
401    Ok(args)
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    fn arg(name: &str, arg_type: Arg) -> CommandArg {
409        CommandArg {
410            name: name.to_string(),
411            arg_type,
412            validation: Validation::Auto,
413            required: true,
414        }
415    }
416
417    #[test]
418    fn parse_double_args() {
419        let schema = vec![
420            arg("x", Arg::Double),
421            arg("y", Arg::Double),
422            arg("z", Arg::Double),
423        ];
424        let result = parse_args("10.5 64.0 -5.0", &schema).unwrap();
425        assert_eq!(result.get_double("x"), Some(10.5));
426        assert_eq!(result.get_double("y"), Some(64.0));
427        assert_eq!(result.get_double("z"), Some(-5.0));
428    }
429
430    #[test]
431    fn parse_integer_args() {
432        let schema = vec![arg("count", Arg::Integer)];
433        let result = parse_args("42", &schema).unwrap();
434        assert_eq!(result.get_integer("count"), Some(42));
435    }
436
437    #[test]
438    fn parse_string_arg() {
439        let schema = vec![arg("name", Arg::String)];
440        let result = parse_args("Steve", &schema).unwrap();
441        assert_eq!(result.get_string("name"), Some("Steve"));
442    }
443
444    #[test]
445    fn parse_options_valid() {
446        let schema = vec![arg(
447            "mode",
448            Arg::Options(vec!["survival".into(), "creative".into()]),
449        )];
450        let result = parse_args("creative", &schema).unwrap();
451        assert_eq!(result.get_string("mode"), Some("creative"));
452    }
453
454    #[test]
455    fn parse_options_invalid() {
456        let schema = vec![arg(
457            "mode",
458            Arg::Options(vec!["survival".into(), "creative".into()]),
459        )];
460        let err = parse_args("hardcore", &schema).unwrap_err();
461        assert!(err.contains("Invalid 'mode'"));
462    }
463
464    #[test]
465    fn parse_options_custom_error() {
466        let schema = vec![CommandArg {
467            name: "mode".into(),
468            arg_type: Arg::Options(vec!["survival".into(), "creative".into()]),
469            validation: Validation::Custom("Nope, bad mode".into()),
470            required: true,
471        }];
472        let err = parse_args("hardcore", &schema).unwrap_err();
473        assert_eq!(err, "Nope, bad mode");
474    }
475
476    #[test]
477    fn parse_double_invalid() {
478        let schema = vec![arg("x", Arg::Double)];
479        let err = parse_args("abc", &schema).unwrap_err();
480        assert!(err.contains("Expected a number"));
481    }
482
483    #[test]
484    fn parse_too_few_args() {
485        let schema = vec![
486            arg("x", Arg::Double),
487            arg("y", Arg::Double),
488            arg("z", Arg::Double),
489        ];
490        let err = parse_args("10.5", &schema).unwrap_err();
491        assert!(err.contains("Usage:"));
492    }
493
494    #[test]
495    fn parse_validation_disabled() {
496        let schema = vec![CommandArg {
497            name: "value".into(),
498            arg_type: Arg::Double,
499            validation: Validation::Disabled,
500            required: true,
501        }];
502        let result = parse_args("abc", &schema).unwrap();
503        assert_eq!(result.get_string("value"), Some("abc"));
504    }
505
506    #[test]
507    fn parse_optional_arg_missing() {
508        let schema = vec![CommandArg {
509            name: "target".into(),
510            arg_type: Arg::String,
511            validation: Validation::Auto,
512            required: false,
513        }];
514        let result = parse_args("", &schema).unwrap();
515        assert_eq!(result.get_string("target"), None);
516    }
517
518    #[test]
519    fn parse_greedy_string() {
520        let schema = vec![arg("msg", Arg::Message)];
521        let result = parse_args("hello world foo", &schema).unwrap();
522        assert_eq!(result.get_string("msg"), Some("hello world foo"));
523    }
524
525    #[test]
526    fn parse_boolean_valid() {
527        let schema = vec![arg("flag", Arg::Boolean)];
528        let result = parse_args("true", &schema).unwrap();
529        assert_eq!(result.get_bool("flag"), Some(true));
530
531        let result = parse_args("false", &schema).unwrap();
532        assert_eq!(result.get_bool("flag"), Some(false));
533    }
534
535    #[test]
536    fn parse_boolean_invalid() {
537        let schema = vec![arg("flag", Arg::Boolean)];
538        let err = parse_args("maybe", &schema).unwrap_err();
539        assert!(err.contains("Expected true/false"));
540    }
541
542    #[test]
543    fn parse_player_arg() {
544        let schema = vec![arg("target", Arg::Player)];
545        let result = parse_args("Steve", &schema).unwrap();
546        assert_eq!(result.get_string("target"), Some("Steve"));
547    }
548
549    #[test]
550    fn parse_variants_first_match() {
551        let v1 = vec![arg("x", Arg::Double), arg("y", Arg::Double)];
552        let v2 = vec![arg("name", Arg::String)];
553        let result = parse_command_args("10.5 20.0", &[], &[v1, v2]).unwrap();
554        assert_eq!(result.get_double("x"), Some(10.5));
555    }
556
557    #[test]
558    fn parse_variants_second_match() {
559        let v1 = vec![arg("x", Arg::Double), arg("y", Arg::Double)];
560        let v2 = vec![arg("name", Arg::Player)];
561        let result = parse_command_args("Steve", &[], &[v1, v2]).unwrap();
562        assert_eq!(result.get_string("name"), Some("Steve"));
563    }
564
565    #[test]
566    fn raw_preserved() {
567        let schema = vec![arg("msg", Arg::String)];
568        let result = parse_args("hello world", &schema).unwrap();
569        assert_eq!(result.raw(), "hello world");
570    }
571
572    #[test]
573    fn parse_vec3_typed() {
574        let schema = vec![arg("pos", Arg::Vec3)];
575        let result = parse_args("10.5 64.0 -5.0", &schema).unwrap();
576        assert_eq!(result.get_vec3("pos"), Some((10.5, 64.0, -5.0)));
577        assert_eq!(result.get_string("pos"), None); // not stored as string
578    }
579
580    #[test]
581    fn parse_vec3_invalid() {
582        let schema = vec![arg("pos", Arg::Vec3)];
583        let err = parse_args("10.5 abc -5.0", &schema).unwrap_err();
584        assert!(err.contains("Invalid coordinate"));
585    }
586
587    #[test]
588    fn parse_block_pos_typed() {
589        let schema = vec![arg("pos", Arg::BlockPos)];
590        let result = parse_args("10 64 -5", &schema).unwrap();
591        assert_eq!(result.get_block_pos("pos"), Some((10, 64, -5)));
592    }
593
594    #[test]
595    fn token_count_method() {
596        assert_eq!(Arg::Vec3.token_count(), 3);
597        assert_eq!(Arg::BlockPos.token_count(), 3);
598        assert_eq!(Arg::Vec2.token_count(), 2);
599        assert_eq!(Arg::Rotation.token_count(), 2);
600        assert_eq!(Arg::Message.token_count(), 0);
601        assert_eq!(Arg::String.token_count(), 1);
602        assert_eq!(Arg::Boolean.token_count(), 1);
603    }
604}