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    uuid::Uuid,
18};
19use common_i18n::Content;
20use common_net::sync::WorldSyncExt;
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
550/// A helper function to get the Uuid of a player with a given alias
551pub fn get_player_uuid(client: &Client, alias: &String) -> Option<Uuid> {
552    client
553        .player_list()
554        .values()
555        .find(|p| p.player_alias == *alias)
556        .map(|p| p.uuid)
557}
558
559trait TabComplete {
560    fn complete(&self, part: &str, client: &Client) -> Vec<String>;
561}
562
563impl TabComplete for ArgumentSpec {
564    fn complete(&self, part: &str, client: &Client) -> Vec<String> {
565        match self {
566            ArgumentSpec::PlayerName(_) => complete_player(part, client),
567            ArgumentSpec::EntityTarget(_) => {
568                if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) {
569                    match spec {
570                        "" => ClientEntityTarget::iter()
571                            .filter_map(|target| {
572                                let ident = target.keyword();
573                                if ident.starts_with(end) {
574                                    Some(format!("@{ident}"))
575                                } else {
576                                    None
577                                }
578                            })
579                            .collect(),
580                        "uid" => {
581                            if let Some(end) =
582                                u64::from_str(end).ok().or(end.is_empty().then_some(0))
583                            {
584                                client
585                                    .state()
586                                    .ecs()
587                                    .read_storage::<Uid>()
588                                    .join()
589                                    .filter_map(|uid| {
590                                        let uid = u64::from(*uid);
591                                        if end < uid {
592                                            Some(format!("uid@{uid}"))
593                                        } else {
594                                            None
595                                        }
596                                    })
597                                    .collect()
598                            } else {
599                                vec![]
600                            }
601                        },
602                        _ => vec![],
603                    }
604                } else {
605                    complete_player(part, client)
606                }
607            },
608            ArgumentSpec::SiteName(_) => complete_site(part, client),
609            ArgumentSpec::Float(_, x, _) => {
610                if part.is_empty() {
611                    vec![format!("{:.1}", x)]
612                } else {
613                    vec![]
614                }
615            },
616            ArgumentSpec::Integer(_, x, _) => {
617                if part.is_empty() {
618                    vec![format!("{}", x)]
619                } else {
620                    vec![]
621                }
622            },
623            ArgumentSpec::Any(_, _) => vec![],
624            ArgumentSpec::Command(_) => complete_command(part, ""),
625            ArgumentSpec::Message(_) => complete_player(part, client),
626            ArgumentSpec::SubCommand => complete_command(part, ""),
627            ArgumentSpec::Enum(_, strings, _) => strings
628                .iter()
629                .filter(|string| string.starts_with(part))
630                .map(|c| c.to_string())
631                .collect(),
632            ArgumentSpec::AssetPath(_, prefix, paths, _) => {
633                if let Some(part_stripped) = part.strip_prefix('#') {
634                    paths
635                        .iter()
636                        .filter(|string| string.contains(part_stripped))
637                        .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
638                        .collect()
639                } else {
640                    let part_with_prefix = prefix.to_string() + part;
641                    let depth = part_with_prefix.split('.').count();
642                    paths
643                        .iter()
644                        .map(|path| path.as_str().split('.').take(depth).join("."))
645                        .dedup()
646                        .filter(|string| string.starts_with(&part_with_prefix))
647                        .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
648                        .collect()
649                }
650            },
651            ArgumentSpec::Boolean(_, part, _) => ["true", "false"]
652                .iter()
653                .filter(|string| string.starts_with(part))
654                .map(|c| c.to_string())
655                .collect(),
656            ArgumentSpec::Flag(part) => vec![part.to_string()],
657        }
658    }
659}
660
661fn complete_player(part: &str, client: &Client) -> Vec<String> {
662    client
663        .player_list()
664        .values()
665        .map(|player_info| &player_info.player_alias)
666        .filter(|alias| alias.starts_with(part))
667        .cloned()
668        .collect()
669}
670
671fn complete_site(mut part: &str, client: &Client) -> Vec<String> {
672    if let Some(p) = part.strip_prefix('"') {
673        part = p;
674    }
675    client
676        .sites()
677        .values()
678        .filter_map(|site| match site.site.kind {
679            common_net::msg::world_msg::SiteKind::Cave => None,
680            _ => site.site.name.as_ref(),
681        })
682        .filter(|name| name.starts_with(part))
683        .map(|name| {
684            if name.contains(' ') {
685                format!("\"{}\"", name)
686            } else {
687                name.clone()
688            }
689        })
690        .collect()
691}
692
693// Get the byte index of the nth word. Used in completing "/sudo p subcmd"
694fn nth_word(line: &str, n: usize) -> Option<usize> {
695    let mut is_space = false;
696    let mut j = 0;
697    for (i, c) in line.char_indices() {
698        match (is_space, c.is_whitespace()) {
699            (true, true) => {},
700            (true, false) => {
701                is_space = false;
702                j += 1;
703            },
704            (false, true) => {
705                is_space = true;
706            },
707            (false, false) => {},
708        }
709        if j == n {
710            return Some(i);
711        }
712    }
713    None
714}
715
716fn complete_command(part: &str, prefix: &str) -> Vec<String> {
717    ServerChatCommand::iter_with_keywords()
718        .map(|(kwd, _)| kwd)
719        .chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
720        .filter(|kwd| kwd.starts_with(part))
721        .map(|kwd| format!("{}{}", prefix, kwd))
722        .collect()
723}
724
725pub fn complete(line: &str, client: &Client, cmd_prefix: &str) -> Vec<String> {
726    let word = if line.chars().last().is_none_or(char::is_whitespace) {
727        ""
728    } else {
729        line.split_whitespace().last().unwrap_or("")
730    };
731
732    if line.starts_with(cmd_prefix) {
733        let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
734        let mut iter = line.split_whitespace();
735        let cmd = iter.next().unwrap_or("");
736        let i = iter.count() + usize::from(word.is_empty());
737        if i == 0 {
738            // Completing chat command name. This is the start of the line so the prefix
739            // will be part of it
740            let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
741            return complete_command(word, cmd_prefix);
742        }
743
744        let args = {
745            if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
746                Some(cmd.data().args)
747            } else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
748                Some(cmd.data().args)
749            } else {
750                None
751            }
752        };
753
754        if let Some(args) = args {
755            if let Some(arg) = args.get(i - 1) {
756                // Complete ith argument
757                arg.complete(word, client)
758            } else {
759                // Complete past the last argument
760                match args.last() {
761                    Some(ArgumentSpec::SubCommand) => {
762                        if let Some(index) = nth_word(line, args.len()) {
763                            complete(&line[index..], client, "")
764                        } else {
765                            vec![]
766                        }
767                    },
768                    Some(ArgumentSpec::Message(_)) => complete_player(word, client),
769                    _ => vec![], // End of command. Nothing to complete
770                }
771            }
772        } else {
773            // Completing for unknown chat command
774            complete_player(word, client)
775        }
776    } else {
777        // Not completing a command
778        complete_player(word, client)
779    }
780}
781
782#[test]
783fn verify_cmd_list_sorted() {
784    let mut list = ClientChatCommand::iter()
785        .map(|c| c.keyword())
786        .collect::<Vec<_>>();
787
788    // Vec::is_sorted is unstable, so we do it the hard way
789    let list2 = list.clone();
790    list.sort_unstable();
791    assert_eq!(list, list2);
792}
793
794#[test]
795fn test_complete_command() {
796    assert_eq!(complete_command("mu", "/"), vec!["/mute".to_string()]);
797    assert_eq!(complete_command("unba", "/"), vec![
798        "/unban".to_string(),
799        "/unban_ip".to_string()
800    ]);
801    assert_eq!(complete_command("make_", "/"), vec![
802        "/make_block".to_string(),
803        "/make_npc".to_string(),
804        "/make_sprite".to_string(),
805        "/make_volume".to_string()
806    ]);
807}