veloren_voxygen/
cmd.rs

1use std::str::FromStr;
2
3use crate::{
4    GlobalState,
5    render::ExperimentalShader,
6    session::{SessionState, settings_change::change_render_mode},
7};
8use client::Client;
9use common::{
10    cmd::*,
11    comp::Admin,
12    link::Is,
13    mounting::{Mount, Rider, VolumeRider},
14    parse_cmd_args,
15    resources::PlayerEntity,
16    uid::Uid,
17};
18use common_i18n::Content;
19use common_net::sync::WorldSyncExt;
20use i18n::Localization;
21use itertools::Itertools;
22use levenshtein::levenshtein;
23use specs::{Join, WorldExt};
24use strum::{EnumIter, IntoEnumIterator};
25
26// Please keep this sorted alphabetically, same as with server commands :-)
27#[derive(Clone, Copy, strum::EnumIter)]
28pub enum ClientChatCommand {
29    Clear,
30    ExperimentalShader,
31    Help,
32    Mute,
33    Unmute,
34}
35
36impl ClientChatCommand {
37    pub fn data(&self) -> ChatCommandData {
38        use ArgumentSpec::*;
39        use Requirement::*;
40        let cmd = ChatCommandData::new;
41        match self {
42            ClientChatCommand::Clear => {
43                cmd(Vec::new(), Content::localized("command-clear-desc"), None)
44            },
45            ClientChatCommand::ExperimentalShader => cmd(
46                vec![Enum(
47                    "Shader",
48                    ExperimentalShader::iter()
49                        .map(|item| item.to_string())
50                        .collect(),
51                    Optional,
52                )],
53                Content::localized("command-experimental_shader-desc"),
54                None,
55            ),
56            ClientChatCommand::Help => cmd(
57                vec![Command(Optional)],
58                Content::localized("command-help-desc"),
59                None,
60            ),
61            ClientChatCommand::Mute => cmd(
62                vec![PlayerName(Required)],
63                Content::localized("command-mute-desc"),
64                None,
65            ),
66            ClientChatCommand::Unmute => cmd(
67                vec![PlayerName(Required)],
68                Content::localized("command-unmute-desc"),
69                None,
70            ),
71        }
72    }
73
74    pub fn keyword(&self) -> &'static str {
75        match self {
76            ClientChatCommand::Clear => "clear",
77            ClientChatCommand::ExperimentalShader => "experimental_shader",
78            ClientChatCommand::Help => "help",
79            ClientChatCommand::Mute => "mute",
80            ClientChatCommand::Unmute => "unmute",
81        }
82    }
83
84    /// A message that explains what the command does
85    pub fn help_content(&self) -> Content {
86        let data = self.data();
87
88        let usage = std::iter::once(format!("/{}", self.keyword()))
89            .chain(data.args.iter().map(|arg| arg.usage_string()))
90            .collect::<Vec<_>>()
91            .join(" ");
92
93        Content::localized_with_args("command-help-template", [
94            ("usage", Content::Plain(usage)),
95            ("description", data.description),
96        ])
97    }
98
99    /// Returns a format string for parsing arguments with scan_fmt
100    pub fn arg_fmt(&self) -> String {
101        self.data()
102            .args
103            .iter()
104            .map(|arg| match arg {
105                ArgumentSpec::PlayerName(_) => "{}",
106                ArgumentSpec::EntityTarget(_) => "{}",
107                ArgumentSpec::SiteName(_) => "{/.*/}",
108                ArgumentSpec::Float(_, _, _) => "{}",
109                ArgumentSpec::Integer(_, _, _) => "{d}",
110                ArgumentSpec::Any(_, _) => "{}",
111                ArgumentSpec::Command(_) => "{}",
112                ArgumentSpec::Message(_) => "{/.*/}",
113                ArgumentSpec::SubCommand => "{} {/.*/}",
114                ArgumentSpec::Enum(_, _, _) => "{}",
115                ArgumentSpec::AssetPath(_, _, _, _) => "{}",
116                ArgumentSpec::Boolean(_, _, _) => "{}",
117                ArgumentSpec::Flag(_) => "{}",
118            })
119            .collect::<Vec<_>>()
120            .join(" ")
121    }
122
123    /// Produce an iterator over all the available commands
124    pub fn iter() -> impl Iterator<Item = Self> + Clone {
125        <Self as strum::IntoEnumIterator>::iter()
126    }
127
128    /// Produce an iterator that first goes over all the short keywords
129    /// and their associated commands and then iterates over all the normal
130    /// keywords with their associated commands
131    pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
132        Self::iter().map(|c| (c.keyword(), c))
133    }
134}
135
136impl FromStr for ClientChatCommand {
137    type Err = ();
138
139    fn from_str(keyword: &str) -> Result<ClientChatCommand, ()> {
140        Self::iter()
141            .map(|c| (c.keyword(), c))
142            .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
143            .ok_or(())
144    }
145}
146
147#[derive(Clone, Copy)]
148pub enum ChatCommandKind {
149    Client(ClientChatCommand),
150    Server(ServerChatCommand),
151}
152
153impl FromStr for ChatCommandKind {
154    type Err = ();
155
156    fn from_str(s: &str) -> Result<Self, ()> {
157        if let Ok(cmd) = s.parse::<ClientChatCommand>() {
158            Ok(ChatCommandKind::Client(cmd))
159        } else if let Ok(cmd) = s.parse::<ServerChatCommand>() {
160            Ok(ChatCommandKind::Server(cmd))
161        } else {
162            Err(())
163        }
164    }
165}
166
167/// Represents the feedback shown to the user of a command, if any. Server
168/// commands give their feedback as an event, so in those cases this will always
169/// be Ok(None). An Err variant will be be displayed with the error icon and
170/// text color
171type CommandResult = Result<Option<String>, String>;
172
173#[derive(EnumIter)]
174enum ClientEntityTarget {
175    Target,
176    Selected,
177    Viewpoint,
178    Mount,
179    Rider,
180    TargetSelf,
181}
182
183impl ClientEntityTarget {
184    const PREFIX: char = '@';
185
186    fn keyword(&self) -> &'static str {
187        match self {
188            ClientEntityTarget::Target => "target",
189            ClientEntityTarget::Selected => "selected",
190            ClientEntityTarget::Viewpoint => "viewpoint",
191            ClientEntityTarget::Mount => "mount",
192            ClientEntityTarget::Rider => "rider",
193            ClientEntityTarget::TargetSelf => "self",
194        }
195    }
196}
197
198fn preproccess_command(
199    session_state: &mut SessionState,
200    command: &ChatCommandKind,
201    args: &mut [String],
202) -> CommandResult {
203    let mut cmd_args = match command {
204        ChatCommandKind::Client(cmd) => cmd.data().args,
205        ChatCommandKind::Server(cmd) => cmd.data().args,
206    };
207    let client = &mut session_state.client.borrow_mut();
208    let ecs = client.state().ecs();
209    let player = ecs.read_resource::<PlayerEntity>().0;
210    let mut command_start = 0;
211    for (i, arg) in args.iter_mut().enumerate() {
212        let mut could_be_entity_target = false;
213        if let Some(post_cmd_args) = cmd_args.get(i - command_start..) {
214            for (j, arg_spec) in post_cmd_args.iter().enumerate() {
215                match arg_spec {
216                    ArgumentSpec::EntityTarget(_) => could_be_entity_target = true,
217                    ArgumentSpec::SubCommand => {
218                        if let Some(sub_command) =
219                            ServerChatCommand::iter().find(|cmd| cmd.keyword() == arg)
220                        {
221                            cmd_args = sub_command.data().args;
222                            command_start = i + j + 1;
223                            break;
224                        }
225                    },
226                    ArgumentSpec::AssetPath(_, prefix, _, _) => {
227                        *arg = prefix.to_string() + arg;
228                    },
229                    _ => {},
230                }
231                if matches!(arg_spec.requirement(), Requirement::Required) {
232                    break;
233                }
234            }
235        } else if matches!(cmd_args.last(), Some(ArgumentSpec::SubCommand)) {
236            could_be_entity_target = true;
237        }
238        if could_be_entity_target && arg.starts_with(ClientEntityTarget::PREFIX) {
239            let target_str = arg.trim_start_matches(ClientEntityTarget::PREFIX);
240            let target = ClientEntityTarget::iter()
241                .find(|t| t.keyword() == target_str)
242                .ok_or_else(|| {
243                    let help_string = ClientEntityTarget::iter()
244                        .map(|t| t.keyword().to_string())
245                        .reduce(|a, b| format!("{a}/{b}"))
246                        .unwrap_or_default();
247                    format!("Expected {help_string} after '@' found {target_str}")
248                })?;
249            let uid = match target {
250                ClientEntityTarget::Target => session_state
251                    .target_entity
252                    .and_then(|e| ecs.uid_from_entity(e))
253                    .ok_or("Not looking at a valid target".to_string())?,
254                ClientEntityTarget::Selected => session_state
255                    .selected_entity
256                    .and_then(|(e, _)| ecs.uid_from_entity(e))
257                    .ok_or("You don't have a valid target selected".to_string())?,
258                ClientEntityTarget::Viewpoint => session_state
259                    .viewpoint_entity
260                    .and_then(|e| ecs.uid_from_entity(e))
261                    .ok_or("Not viewing from a valid viewpoint entity".to_string())?,
262                ClientEntityTarget::Mount => {
263                    if let Some(player) = player {
264                        ecs.read_storage::<Is<Rider>>()
265                            .get(player)
266                            .map(|is_rider| is_rider.mount)
267                            .or(ecs.read_storage::<Is<VolumeRider>>().get(player).and_then(
268                                |is_rider| match is_rider.pos.kind {
269                                    common::mounting::Volume::Terrain => None,
270                                    common::mounting::Volume::Entity(uid) => Some(uid),
271                                },
272                            ))
273                            .ok_or("Not riding a valid entity".to_string())?
274                    } else {
275                        return Err("No player entity".to_string());
276                    }
277                },
278                ClientEntityTarget::Rider => {
279                    if let Some(player) = player {
280                        ecs.read_storage::<Is<Mount>>()
281                            .get(player)
282                            .map(|is_mount| is_mount.rider)
283                            .ok_or("No valid rider".to_string())?
284                    } else {
285                        return Err("No player entity".to_string());
286                    }
287                },
288                ClientEntityTarget::TargetSelf => player
289                    .and_then(|e| ecs.uid_from_entity(e))
290                    .ok_or("No player entity")?,
291            };
292            let uid = u64::from(uid);
293            *arg = format!("uid@{uid}");
294        }
295    }
296
297    Ok(None)
298}
299
300/// Runs a command by either sending it to the server or processing it
301/// locally. Returns a String to be output to the chat.
302// Note: it's not clear what data future commands will need access to, so the
303// signature of this function might change
304pub fn run_command(
305    session_state: &mut SessionState,
306    global_state: &mut GlobalState,
307    cmd: &str,
308    mut args: Vec<String>,
309) -> CommandResult {
310    let command = ChatCommandKind::from_str(cmd)
311        .map_err(|_| invalid_command_message(&session_state.client.borrow(), cmd.to_string()))?;
312
313    preproccess_command(session_state, &command, &mut args)?;
314
315    match command {
316        ChatCommandKind::Server(cmd) => {
317            session_state
318                .client
319                .borrow_mut()
320                .send_command(cmd.keyword().into(), args);
321            Ok(None) // The server will provide a response when the command is
322            // run
323        },
324        ChatCommandKind::Client(cmd) => run_client_command(session_state, global_state, cmd, args),
325    }
326}
327
328fn invalid_command_message(client: &Client, user_entered_invalid_command: String) -> String {
329    let entity_role = client
330        .state()
331        .read_storage::<Admin>()
332        .get(client.entity())
333        .map(|admin| admin.0);
334
335    let usable_commands = ServerChatCommand::iter()
336        .filter(|cmd| cmd.needs_role() <= entity_role)
337        .map(|cmd| cmd.keyword())
338        .chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()));
339
340    let most_similar_str = usable_commands
341        .clone()
342        .min_by_key(|cmd| levenshtein(&user_entered_invalid_command, cmd))
343        .expect("At least one command exists.");
344
345    let commands_with_same_prefix = usable_commands
346        .filter(|cmd| cmd.starts_with(&user_entered_invalid_command) && cmd != &most_similar_str);
347
348    format!(
349        "Could not find a command named {}. Did you mean any of the following? \n/{} {} \n\nType \
350         /help to see a list of all commands.",
351        user_entered_invalid_command,
352        most_similar_str,
353        commands_with_same_prefix.fold(String::new(), |s, arg| s + "\n/" + arg)
354    )
355}
356
357fn run_client_command(
358    session_state: &mut SessionState,
359    global_state: &mut GlobalState,
360    command: ClientChatCommand,
361    args: Vec<String>,
362) -> CommandResult {
363    let command = match command {
364        ClientChatCommand::Clear => handle_clear,
365        ClientChatCommand::ExperimentalShader => handle_experimental_shader,
366        ClientChatCommand::Help => handle_help,
367        ClientChatCommand::Mute => handle_mute,
368        ClientChatCommand::Unmute => handle_unmute,
369    };
370
371    command(session_state, global_state, args)
372}
373
374fn handle_clear(
375    session_state: &mut SessionState,
376    _global_state: &mut GlobalState,
377    _args: Vec<String>,
378) -> CommandResult {
379    session_state.hud.clear_chat();
380    Ok(None)
381}
382
383fn handle_help(
384    session_state: &mut SessionState,
385    global_state: &mut GlobalState,
386    args: Vec<String>,
387) -> CommandResult {
388    let i18n = global_state.i18n.read();
389
390    if let Some(cmd) = parse_cmd_args!(&args, ServerChatCommand) {
391        Ok(Some(i18n.get_content(&cmd.help_content())))
392    } else if let Some(cmd) = parse_cmd_args!(&args, ClientChatCommand) {
393        Ok(Some(i18n.get_content(&cmd.help_content())))
394    } else {
395        let client = &mut session_state.client.borrow_mut();
396
397        let mut message = String::new();
398        let entity_role = client
399            .state()
400            .read_storage::<Admin>()
401            .get(client.entity())
402            .map(|admin| admin.0);
403
404        ClientChatCommand::iter().for_each(|cmd| {
405            message += &i18n.get_content(&cmd.help_content());
406            message += "\n";
407        });
408        // Iterate through all ServerChatCommands you have permission to use.
409        ServerChatCommand::iter()
410            .filter(|cmd| cmd.needs_role() <= entity_role)
411            .for_each(|cmd| {
412                message += &i18n.get_content(&cmd.help_content());
413                message += "\n";
414            });
415        message += &i18n.get_msg("command-help-additional-shortcuts");
416        ServerChatCommand::iter()
417            .filter(|cmd| cmd.needs_role() <= entity_role)
418            .filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
419            .for_each(|(k, cmd)| {
420                message += &format!(" /{} => /{}", k, cmd.keyword());
421            });
422        Ok(Some(message))
423    }
424}
425
426fn handle_mute(
427    session_state: &mut SessionState,
428    global_state: &mut GlobalState,
429    args: Vec<String>,
430) -> CommandResult {
431    if let Some(alias) = parse_cmd_args!(args, String) {
432        let client = &mut session_state.client.borrow_mut();
433
434        let target = client
435            .player_list()
436            .values()
437            .find(|p| p.player_alias == alias)
438            .ok_or_else(|| format!("Could not find a player named {}", alias))?;
439
440        if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid)) {
441            if target.uuid == me.uuid {
442                return Err("You cannot mute yourself.".to_string());
443            }
444        }
445
446        if global_state
447            .profile
448            .mutelist
449            .insert(target.uuid, alias.clone())
450            .is_none()
451        {
452            Ok(Some(format!("Successfully muted player {}.", alias)))
453        } else {
454            Err(format!("{} is already muted.", alias))
455        }
456    } else {
457        Err("You must specify a player to mute.".to_string())
458    }
459}
460
461fn handle_unmute(
462    session_state: &mut SessionState,
463    global_state: &mut GlobalState,
464    args: Vec<String>,
465) -> CommandResult {
466    // Note that we don't care if this is a real player, so that it's possible
467    // to unmute someone when they're offline
468    if let Some(alias) = parse_cmd_args!(args, String) {
469        if let Some(uuid) = global_state
470            .profile
471            .mutelist
472            .iter()
473            .find(|(_, v)| **v == alias)
474            .map(|(k, _)| *k)
475        {
476            let client = &mut session_state.client.borrow_mut();
477
478            if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid)) {
479                if uuid == me.uuid {
480                    return Err("You cannot unmute yourself.".to_string());
481                }
482            }
483
484            global_state.profile.mutelist.remove(&uuid);
485            Ok(Some(format!("Successfully unmuted player {}.", alias)))
486        } else {
487            Err(format!("Could not find a muted player named {}.", alias))
488        }
489    } else {
490        Err("You must specify a player to unmute.".to_string())
491    }
492}
493
494fn handle_experimental_shader(
495    _session_state: &mut SessionState,
496    global_state: &mut GlobalState,
497    args: Vec<String>,
498) -> CommandResult {
499    if args.is_empty() {
500        Ok(Some(
501            ExperimentalShader::iter()
502                .map(|s| {
503                    let is_active = global_state
504                        .settings
505                        .graphics
506                        .render_mode
507                        .experimental_shaders
508                        .contains(&s);
509                    format!("[{}] {}", if is_active { "x" } else { "  " }, s)
510                })
511                .reduce(|mut a, b| {
512                    a.push('\n');
513                    a.push_str(&b);
514                    a
515                })
516                .ok_or("There are no experimental shaders.".to_string())?,
517        ))
518    } else if let Some(item) = parse_cmd_args!(args, String) {
519        if let Ok(shader) = ExperimentalShader::from_str(&item) {
520            let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
521            let res = if new_render_mode.experimental_shaders.remove(&shader) {
522                Ok(Some(format!("Disabled {item}.")))
523            } else {
524                new_render_mode.experimental_shaders.insert(shader);
525                Ok(Some(format!("Enabled {item}.")))
526            };
527
528            change_render_mode(
529                new_render_mode,
530                &mut global_state.window,
531                &mut global_state.settings,
532            );
533
534            res
535        } else {
536            Err(format!(
537                "{item} is not an expermimental shader, use this command with any arguments to \
538                 see a complete list."
539            ))
540        }
541    } else {
542        Err(
543            "You must specify a valid experimental shader, to get a list of experimental shaders, \
544             use this command without any arguments."
545                .to_string(),
546        )
547    }
548}
549
550trait TabComplete {
551    fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String>;
552}
553
554impl TabComplete for ArgumentSpec {
555    fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
556        match self {
557            ArgumentSpec::PlayerName(_) => complete_player(part, client),
558            ArgumentSpec::EntityTarget(_) => {
559                if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) {
560                    match spec {
561                        "" => ClientEntityTarget::iter()
562                            .filter_map(|target| {
563                                let ident = target.keyword();
564                                if ident.starts_with(end) {
565                                    Some(format!("@{ident}"))
566                                } else {
567                                    None
568                                }
569                            })
570                            .collect(),
571                        "uid" => {
572                            if let Some(end) =
573                                u64::from_str(end).ok().or(end.is_empty().then_some(0))
574                            {
575                                client
576                                    .state()
577                                    .ecs()
578                                    .read_storage::<Uid>()
579                                    .join()
580                                    .filter_map(|uid| {
581                                        let uid = u64::from(*uid);
582                                        if end < uid {
583                                            Some(format!("uid@{uid}"))
584                                        } else {
585                                            None
586                                        }
587                                    })
588                                    .collect()
589                            } else {
590                                vec![]
591                            }
592                        },
593                        _ => vec![],
594                    }
595                } else {
596                    complete_player(part, client)
597                }
598            },
599            ArgumentSpec::SiteName(_) => complete_site(part, client, i18n),
600            ArgumentSpec::Float(_, x, _) => {
601                if part.is_empty() {
602                    vec![format!("{:.1}", x)]
603                } else {
604                    vec![]
605                }
606            },
607            ArgumentSpec::Integer(_, x, _) => {
608                if part.is_empty() {
609                    vec![format!("{}", x)]
610                } else {
611                    vec![]
612                }
613            },
614            ArgumentSpec::Any(_, _) => vec![],
615            ArgumentSpec::Command(_) => complete_command(part, ""),
616            ArgumentSpec::Message(_) => complete_player(part, client),
617            ArgumentSpec::SubCommand => complete_command(part, ""),
618            ArgumentSpec::Enum(_, strings, _) => strings
619                .iter()
620                .filter(|string| string.starts_with(part))
621                .map(|c| c.to_string())
622                .collect(),
623            ArgumentSpec::AssetPath(_, prefix, paths, _) => {
624                if let Some(part_stripped) = part.strip_prefix('#') {
625                    paths
626                        .iter()
627                        .filter(|string| string.contains(part_stripped))
628                        .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
629                        .collect()
630                } else {
631                    let part_with_prefix = prefix.to_string() + part;
632                    let depth = part_with_prefix.split('.').count();
633                    paths
634                        .iter()
635                        .map(|path| path.as_str().split('.').take(depth).join("."))
636                        .dedup()
637                        .filter(|string| string.starts_with(&part_with_prefix))
638                        .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
639                        .collect()
640                }
641            },
642            ArgumentSpec::Boolean(_, part, _) => ["true", "false"]
643                .iter()
644                .filter(|string| string.starts_with(part))
645                .map(|c| c.to_string())
646                .collect(),
647            ArgumentSpec::Flag(part) => vec![part.to_string()],
648        }
649    }
650}
651
652fn complete_player(part: &str, client: &Client) -> Vec<String> {
653    client
654        .player_list()
655        .values()
656        .map(|player_info| &player_info.player_alias)
657        .filter(|alias| alias.starts_with(part))
658        .cloned()
659        .collect()
660}
661
662fn complete_site(mut part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
663    if let Some(p) = part.strip_prefix('"') {
664        part = p;
665    }
666    client
667        .sites()
668        .values()
669        .filter_map(|site| match site.marker.kind {
670            common_net::msg::world_msg::MarkerKind::Cave => None,
671            _ => Some(i18n.get_content(site.marker.name.as_ref()?)),
672        })
673        .filter(|name| name.starts_with(part))
674        .map(|name| {
675            if name.contains(' ') {
676                format!("\"{}\"", name)
677            } else {
678                name.clone()
679            }
680        })
681        .collect()
682}
683
684// Get the byte index of the nth word. Used in completing "/sudo p subcmd"
685fn nth_word(line: &str, n: usize) -> Option<usize> {
686    let mut is_space = false;
687    let mut j = 0;
688    for (i, c) in line.char_indices() {
689        match (is_space, c.is_whitespace()) {
690            (true, true) => {},
691            (true, false) => {
692                is_space = false;
693                j += 1;
694            },
695            (false, true) => {
696                is_space = true;
697            },
698            (false, false) => {},
699        }
700        if j == n {
701            return Some(i);
702        }
703    }
704    None
705}
706
707fn complete_command(part: &str, prefix: &str) -> Vec<String> {
708    ServerChatCommand::iter_with_keywords()
709        .map(|(kwd, _)| kwd)
710        .chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
711        .filter(|kwd| kwd.starts_with(part))
712        .map(|kwd| format!("{}{}", prefix, kwd))
713        .collect()
714}
715
716pub fn complete(line: &str, client: &Client, i18n: &Localization, cmd_prefix: &str) -> Vec<String> {
717    let word = if line.chars().last().is_none_or(char::is_whitespace) {
718        ""
719    } else {
720        line.split_whitespace().last().unwrap_or("")
721    };
722
723    if line.starts_with(cmd_prefix) {
724        let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
725        let mut iter = line.split_whitespace();
726        let cmd = iter.next().unwrap_or("");
727        let i = iter.count() + usize::from(word.is_empty());
728        if i == 0 {
729            // Completing chat command name. This is the start of the line so the prefix
730            // will be part of it
731            let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
732            return complete_command(word, cmd_prefix);
733        }
734
735        let args = {
736            if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
737                Some(cmd.data().args)
738            } else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
739                Some(cmd.data().args)
740            } else {
741                None
742            }
743        };
744
745        if let Some(args) = args {
746            if let Some(arg) = args.get(i - 1) {
747                // Complete ith argument
748                arg.complete(word, client, i18n)
749            } else {
750                // Complete past the last argument
751                match args.last() {
752                    Some(ArgumentSpec::SubCommand) => {
753                        if let Some(index) = nth_word(line, args.len()) {
754                            complete(&line[index..], client, i18n, "")
755                        } else {
756                            vec![]
757                        }
758                    },
759                    Some(ArgumentSpec::Message(_)) => complete_player(word, client),
760                    _ => vec![], // End of command. Nothing to complete
761                }
762            }
763        } else {
764            // Completing for unknown chat command
765            complete_player(word, client)
766        }
767    } else {
768        // Not completing a command
769        complete_player(word, client)
770    }
771}
772
773#[test]
774fn verify_cmd_list_sorted() {
775    let mut list = ClientChatCommand::iter()
776        .map(|c| c.keyword())
777        .collect::<Vec<_>>();
778
779    // Vec::is_sorted is unstable, so we do it the hard way
780    let list2 = list.clone();
781    list.sort_unstable();
782    assert_eq!(list, list2);
783}
784
785#[test]
786fn test_complete_command() {
787    assert_eq!(complete_command("mu", "/"), vec!["/mute".to_string()]);
788    assert_eq!(complete_command("unba", "/"), vec![
789        "/unban".to_string(),
790        "/unban_ip".to_string()
791    ]);
792    assert_eq!(complete_command("make_", "/"), vec![
793        "/make_block".to_string(),
794        "/make_npc".to_string(),
795        "/make_sprite".to_string(),
796        "/make_volume".to_string()
797    ]);
798}