1use std::str::FromStr;
14
15use crate::{
16 GlobalState,
17 render::ExperimentalShader,
18 session::{SessionState, settings_change::change_render_mode},
19};
20use client::Client;
21use common::{
22 cmd::*,
23 comp::Admin,
24 link::Is,
25 mounting::{Mount, Rider, VolumeRider},
26 parse_cmd_args,
27 resources::PlayerEntity,
28 uid::Uid,
29};
30use common_i18n::{Content, LocalizationArg};
31use common_net::sync::WorldSyncExt;
32use i18n::Localization;
33use itertools::Itertools;
34use levenshtein::levenshtein;
35use specs::{Join, WorldExt};
36use strum::{EnumIter, IntoEnumIterator};
37
38#[derive(Clone, Copy, strum::EnumIter)]
46pub enum ClientChatCommand {
47 Clear,
49 ExperimentalShader,
51 Help,
53 Mute,
55 Unmute,
57 Waypoint,
60 Wiki,
62}
63
64impl ClientChatCommand {
65 pub fn data(&self) -> ChatCommandData {
71 use ArgumentSpec::*;
72 use Requirement::*;
73 let cmd = ChatCommandData::new;
74 match self {
75 ClientChatCommand::Clear => {
76 cmd(Vec::new(), Content::localized("command-clear-desc"), None)
77 },
78 ClientChatCommand::ExperimentalShader => cmd(
79 vec![Enum(
80 "Shader",
81 ExperimentalShader::iter()
82 .map(|item| item.to_string())
83 .collect(),
84 Optional,
85 )],
86 Content::localized("command-experimental_shader-desc"),
87 None,
88 ),
89 ClientChatCommand::Help => cmd(
90 vec![Command(Optional)],
91 Content::localized("command-help-desc"),
92 None,
93 ),
94 ClientChatCommand::Mute => cmd(
95 vec![PlayerName(Required)],
96 Content::localized("command-mute-desc"),
97 None,
98 ),
99 ClientChatCommand::Unmute => cmd(
100 vec![PlayerName(Required)],
101 Content::localized("command-unmute-desc"),
102 None,
103 ),
104 ClientChatCommand::Waypoint => {
105 cmd(vec![], Content::localized("command-waypoint-desc"), None)
106 },
107 ClientChatCommand::Wiki => cmd(
108 vec![Any("topic", Optional)],
109 Content::localized("command-wiki-desc"),
110 None,
111 ),
112 }
113 }
114
115 pub fn keyword(&self) -> &'static str {
119 match self {
120 ClientChatCommand::Clear => "clear",
121 ClientChatCommand::ExperimentalShader => "experimental_shader",
122 ClientChatCommand::Help => "help",
123 ClientChatCommand::Mute => "mute",
124 ClientChatCommand::Unmute => "unmute",
125 ClientChatCommand::Waypoint => "waypoint",
126 ClientChatCommand::Wiki => "wiki",
127 }
128 }
129
130 pub fn help_content(&self) -> Content {
132 let data = self.data();
133
134 let usage = std::iter::once(format!("/{}", self.keyword()))
135 .chain(data.args.iter().map(|arg| arg.usage_string()))
136 .collect::<Vec<_>>()
137 .join(" ");
138
139 Content::localized_with_args("command-help-template", [
140 ("usage", Content::Plain(usage)),
141 ("description", data.description),
142 ])
143 }
144
145 pub fn iter() -> impl Iterator<Item = Self> + Clone {
147 <Self as strum::IntoEnumIterator>::iter()
148 }
149
150 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
154 Self::iter().map(|c| (c.keyword(), c))
155 }
156}
157
158impl FromStr for ClientChatCommand {
159 type Err = ();
160
161 fn from_str(keyword: &str) -> Result<ClientChatCommand, ()> {
162 Self::iter()
163 .map(|c| (c.keyword(), c))
164 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
165 .ok_or(())
166 }
167}
168
169#[derive(Clone, Copy)]
175pub enum ChatCommandKind {
176 Client(ClientChatCommand),
177 Server(ServerChatCommand),
178}
179
180impl FromStr for ChatCommandKind {
181 type Err = ();
182
183 fn from_str(s: &str) -> Result<Self, ()> {
184 if let Ok(cmd) = s.parse::<ClientChatCommand>() {
185 Ok(ChatCommandKind::Client(cmd))
186 } else if let Ok(cmd) = s.parse::<ServerChatCommand>() {
187 Ok(ChatCommandKind::Server(cmd))
188 } else {
189 Err(())
190 }
191 }
192}
193
194type CommandResult = Result<Option<Content>, Content>;
203
204#[derive(EnumIter)]
209enum ClientEntityTarget {
210 Target,
212 Selected,
214 Viewpoint,
216 Mount,
218 Rider,
220 TargetSelf,
222}
223
224impl ClientEntityTarget {
225 const PREFIX: char = '@';
226
227 fn keyword(&self) -> &'static str {
228 match self {
229 ClientEntityTarget::Target => "target",
230 ClientEntityTarget::Selected => "selected",
231 ClientEntityTarget::Viewpoint => "viewpoint",
232 ClientEntityTarget::Mount => "mount",
233 ClientEntityTarget::Rider => "rider",
234 ClientEntityTarget::TargetSelf => "self",
235 }
236 }
237}
238
239fn preproccess_command(
245 session_state: &mut SessionState,
246 command: &ChatCommandKind,
247 args: &mut [String],
248) -> CommandResult {
249 let mut cmd_args = match command {
251 ChatCommandKind::Client(cmd) => cmd.data().args,
252 ChatCommandKind::Server(cmd) => cmd.data().args,
253 };
254 let client = &mut session_state.client.borrow_mut();
255 let ecs = client.state().ecs();
256 let player = ecs.read_resource::<PlayerEntity>().0;
257
258 let mut command_start = 0;
259
260 for (i, arg) in args.iter_mut().enumerate() {
261 let mut could_be_entity_target = false;
262
263 if let Some(post_cmd_args) = cmd_args.get(i - command_start..) {
264 for (j, arg_spec) in post_cmd_args.iter().enumerate() {
265 match arg_spec {
266 ArgumentSpec::EntityTarget(_) => could_be_entity_target = true,
267
268 ArgumentSpec::SubCommand => {
269 if let Some(sub_command) =
270 ServerChatCommand::iter().find(|cmd| cmd.keyword() == arg)
271 {
272 cmd_args = sub_command.data().args;
273 command_start = i + j + 1;
274 break;
275 }
276 },
277
278 ArgumentSpec::AssetPath(_, prefix, _, _) => {
279 *arg = prefix.to_string() + arg;
280 },
281 _ => {},
282 }
283
284 if matches!(arg_spec.requirement(), Requirement::Required) {
285 break;
286 }
287 }
288 } else if matches!(cmd_args.last(), Some(ArgumentSpec::SubCommand)) {
289 could_be_entity_target = true;
292 }
293 if could_be_entity_target && arg.starts_with(ClientEntityTarget::PREFIX) {
295 let target_str = arg.trim_start_matches(ClientEntityTarget::PREFIX);
297
298 let target = ClientEntityTarget::iter()
300 .find(|t| t.keyword() == target_str)
301 .ok_or_else(|| {
302 let expected_list = ClientEntityTarget::iter()
304 .map(|t| t.keyword().to_string())
305 .collect::<Vec<String>>()
306 .join("/");
307 Content::localized_with_args("command-preprocess-target-error", [
308 ("expected_list", LocalizationArg::from(expected_list)),
309 ("target", LocalizationArg::from(target_str)),
310 ])
311 })?;
312 let uid = match target {
313 ClientEntityTarget::Target => session_state
314 .target_entity
315 .and_then(|e| ecs.uid_from_entity(e))
316 .ok_or(Content::localized(
317 "command-preprocess-not-looking-at-valid-target",
318 ))?,
319 ClientEntityTarget::Selected => session_state
320 .selected_entity
321 .and_then(|(e, _)| ecs.uid_from_entity(e))
322 .ok_or(Content::localized(
323 "command-preprocess-not-selected-valid-target",
324 ))?,
325 ClientEntityTarget::Viewpoint => session_state
326 .viewpoint_entity
327 .and_then(|e| ecs.uid_from_entity(e))
328 .ok_or(Content::localized(
329 "command-preprocess-not-valid-viewpoint-entity",
330 ))?,
331 ClientEntityTarget::Mount => {
332 if let Some(player) = player {
333 ecs.read_storage::<Is<Rider>>()
334 .get(player)
335 .map(|is_rider| is_rider.mount)
336 .or(ecs.read_storage::<Is<VolumeRider>>().get(player).and_then(
337 |is_rider| match is_rider.pos.kind {
338 common::mounting::Volume::Terrain => None,
339 common::mounting::Volume::Entity(uid) => Some(uid),
340 },
341 ))
342 .ok_or(Content::localized(
343 "command-preprocess-not-riding-valid-entity",
344 ))?
345 } else {
346 return Err(Content::localized("command-preprocess-no-player-entity"));
347 }
348 },
349 ClientEntityTarget::Rider => {
350 if let Some(player) = player {
351 ecs.read_storage::<Is<Mount>>()
352 .get(player)
353 .map(|is_mount| is_mount.rider)
354 .ok_or(Content::localized("command-preprocess-not-valid-rider"))?
355 } else {
356 return Err(Content::localized("command-preprocess-no-player-entity"));
357 }
358 },
359 ClientEntityTarget::TargetSelf => player
360 .and_then(|e| ecs.uid_from_entity(e))
361 .ok_or(Content::localized("command-preprocess-no-player-entity"))?,
362 };
363
364 let uid = u64::from(uid);
366 *arg = format!("uid@{uid}");
367 }
368 }
369
370 Ok(None)
371}
372
373pub fn run_command(
380 session_state: &mut SessionState,
381 global_state: &mut GlobalState,
382 cmd: &str,
383 mut args: Vec<String>,
384) -> CommandResult {
385 let command = ChatCommandKind::from_str(cmd)
386 .map_err(|_| invalid_command_message(&session_state.client.borrow(), cmd.to_string()))?;
387
388 preproccess_command(session_state, &command, &mut args)?;
389
390 match command {
391 ChatCommandKind::Server(cmd) => {
392 session_state
393 .client
394 .borrow_mut()
395 .send_command(cmd.keyword().into(), args);
396 Ok(None) },
399 ChatCommandKind::Client(cmd) => run_client_command(session_state, global_state, cmd, args),
400 }
401}
402
403fn invalid_command_message(client: &Client, user_entered_invalid_command: String) -> Content {
405 let entity_role = client
406 .state()
407 .read_storage::<Admin>()
408 .get(client.entity())
409 .map(|admin| admin.0);
410
411 let usable_commands = ServerChatCommand::iter()
412 .filter(|cmd| cmd.needs_role() <= entity_role)
413 .map(|cmd| cmd.keyword())
414 .chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()));
415
416 let most_similar_cmd = usable_commands
417 .clone()
418 .min_by_key(|cmd| levenshtein(&user_entered_invalid_command, cmd))
419 .expect("At least one command exists.");
420
421 let commands_with_same_prefix = usable_commands
422 .filter(|cmd| cmd.starts_with(&user_entered_invalid_command) && cmd != &most_similar_cmd);
423
424 Content::localized_with_args("command-invalid-command-message", [
425 (
426 "invalid-command",
427 LocalizationArg::from(user_entered_invalid_command.clone()),
428 ),
429 (
430 "most-similar-command",
431 LocalizationArg::from(String::from("/") + most_similar_cmd),
432 ),
433 (
434 "commands-with-same-prefix",
435 LocalizationArg::from(
436 commands_with_same_prefix
437 .map(|cmd| format!("/{cmd}"))
438 .collect::<String>(),
439 ),
440 ),
441 ])
442}
443
444fn run_client_command(
449 session_state: &mut SessionState,
450 global_state: &mut GlobalState,
451 command: ClientChatCommand,
452 args: Vec<String>,
453) -> CommandResult {
454 let command = match command {
455 ClientChatCommand::Clear => handle_clear,
456 ClientChatCommand::ExperimentalShader => handle_experimental_shader,
457 ClientChatCommand::Help => handle_help,
458 ClientChatCommand::Mute => handle_mute,
459 ClientChatCommand::Unmute => handle_unmute,
460 ClientChatCommand::Waypoint => handle_waypoint,
461 ClientChatCommand::Wiki => handle_wiki,
462 };
463
464 command(session_state, global_state, args)
465}
466
467fn handle_clear(
469 session_state: &mut SessionState,
470 _global_state: &mut GlobalState,
471 _args: Vec<String>,
472) -> CommandResult {
473 session_state.hud.clear_chat();
474 Ok(None)
475}
476
477fn handle_help(
483 session_state: &mut SessionState,
484 global_state: &mut GlobalState,
485 args: Vec<String>,
486) -> CommandResult {
487 let i18n = global_state.i18n.read();
488
489 if let Some(cmd) = parse_cmd_args!(&args, ServerChatCommand) {
490 Ok(Some(cmd.help_content()))
491 } else if let Some(cmd) = parse_cmd_args!(&args, ClientChatCommand) {
492 Ok(Some(cmd.help_content()))
493 } else {
494 let client = &mut session_state.client.borrow_mut();
495
496 let entity_role = client
497 .state()
498 .read_storage::<Admin>()
499 .get(client.entity())
500 .map(|admin| admin.0);
501
502 let client_commands = ClientChatCommand::iter()
503 .map(|cmd| i18n.get_content(&cmd.help_content()))
504 .join("\n");
505
506 let server_commands = ServerChatCommand::iter()
508 .filter(|cmd| cmd.needs_role() <= entity_role)
509 .map(|cmd| i18n.get_content(&cmd.help_content()))
510 .join("\n");
511
512 let additional_shortcuts = ServerChatCommand::iter()
513 .filter(|cmd| cmd.needs_role() <= entity_role)
514 .filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
515 .map(|(k, cmd)| format!("/{} => /{}", k, cmd.keyword()))
516 .join("\n");
517
518 Ok(Some(Content::localized_with_args("command-help-list", [
519 ("client-commands", LocalizationArg::from(client_commands)),
520 ("server-commands", LocalizationArg::from(server_commands)),
521 (
522 "additional-shortcuts",
523 LocalizationArg::from(additional_shortcuts),
524 ),
525 ])))
526 }
527}
528
529fn handle_mute(
531 session_state: &mut SessionState,
532 global_state: &mut GlobalState,
533 args: Vec<String>,
534) -> CommandResult {
535 if let Some(alias) = parse_cmd_args!(args, String) {
536 let client = &mut session_state.client.borrow_mut();
537
538 let target = client
539 .player_list()
540 .values()
541 .find(|p| p.player_alias == alias)
542 .ok_or_else(|| {
543 Content::localized_with_args("command-mute-no-player-found", [(
544 "player",
545 LocalizationArg::from(alias.clone()),
546 )])
547 })?;
548
549 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid)) {
550 if target.uuid == me.uuid {
551 return Err(Content::localized("command-mute-cannot-mute-self"));
552 }
553 }
554
555 if global_state
556 .profile
557 .mutelist
558 .insert(target.uuid, alias.clone())
559 .is_none()
560 {
561 Ok(Some(Content::localized_with_args(
562 "command-mute-success",
563 [("player", LocalizationArg::from(alias))],
564 )))
565 } else {
566 Err(Content::localized_with_args(
567 "command-mute-already-muted",
568 [("player", LocalizationArg::from(alias))],
569 ))
570 }
571 } else {
572 Err(Content::localized("command-mute-no-player-specified"))
573 }
574}
575
576fn handle_unmute(
578 session_state: &mut SessionState,
579 global_state: &mut GlobalState,
580 args: Vec<String>,
581) -> CommandResult {
582 if let Some(alias) = parse_cmd_args!(args, String) {
585 if let Some(uuid) = global_state
586 .profile
587 .mutelist
588 .iter()
589 .find(|(_, v)| **v == alias)
590 .map(|(k, _)| *k)
591 {
592 let client = &mut session_state.client.borrow_mut();
593
594 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid)) {
595 if uuid == me.uuid {
596 return Err(Content::localized("command-unmute-cannot-unmute-self"));
597 }
598 }
599
600 global_state.profile.mutelist.remove(&uuid);
601
602 Ok(Some(Content::localized_with_args(
603 "command-unmute-success",
604 [("player", LocalizationArg::from(alias))],
605 )))
606 } else {
607 Err(Content::localized_with_args(
608 "command-unmute-no-muted-player-found",
609 [("player", LocalizationArg::from(alias))],
610 ))
611 }
612 } else {
613 Err(Content::localized("command-unmute-no-player-specified"))
614 }
615}
616
617fn handle_experimental_shader(
619 _session_state: &mut SessionState,
620 global_state: &mut GlobalState,
621 args: Vec<String>,
622) -> CommandResult {
623 if args.is_empty() {
624 Ok(Some(Content::localized_with_args(
625 "command-experimental-shaders-list",
626 [(
627 "shader-list",
628 LocalizationArg::from(
629 ExperimentalShader::iter()
630 .map(|s| {
631 let is_active = global_state
632 .settings
633 .graphics
634 .render_mode
635 .experimental_shaders
636 .contains(&s);
637 format!("[{}] {}", if is_active { "x" } else { " " }, s)
638 })
639 .collect::<Vec<String>>()
640 .join("/"),
641 ),
642 )],
643 )))
644 } else if let Some(item) = parse_cmd_args!(args, String) {
645 if let Ok(shader) = ExperimentalShader::from_str(&item) {
646 let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
647 let res = if new_render_mode.experimental_shaders.remove(&shader) {
648 Ok(Some(Content::localized_with_args(
649 "command-experimental-shaders-disabled",
650 [("shader", LocalizationArg::from(item))],
651 )))
652 } else {
653 new_render_mode.experimental_shaders.insert(shader);
654 Ok(Some(Content::localized_with_args(
655 "command-experimental-shaders-enabled",
656 [("shader", LocalizationArg::from(item))],
657 )))
658 };
659
660 change_render_mode(
661 new_render_mode,
662 &mut global_state.window,
663 &mut global_state.settings,
664 );
665
666 res
667 } else {
668 Err(Content::localized_with_args(
669 "command-experimental-shaders-not-a-shader",
670 [("shader", LocalizationArg::from(item))],
671 ))
672 }
673 } else {
674 Err(Content::localized("command-experimental-shaders-not-valid"))
675 }
676}
677
678fn handle_waypoint(
680 session_state: &mut SessionState,
681 _global_state: &mut GlobalState,
682 _args: Vec<String>,
683) -> CommandResult {
684 let client = &mut session_state.client.borrow();
685
686 if let Some(waypoint) = client.waypoint() {
687 Ok(Some(Content::localized_with_args(
688 "command-waypoint-result",
689 [("waypoint", LocalizationArg::from(waypoint.clone()))],
690 )))
691 } else {
692 Err(Content::localized("command-waypoint-error"))
693 }
694}
695
696fn handle_wiki(
702 _session_state: &mut SessionState,
703 _global_state: &mut GlobalState,
704 args: Vec<String>,
705) -> CommandResult {
706 let url = if args.is_empty() {
707 "https://wiki.veloren.net/".to_string()
708 } else {
709 let query_string = args.join("+");
710
711 format!("https://wiki.veloren.net/w/index.php?search={query_string}")
712 };
713
714 open::that_detached(url)
715 .map(|_| Some(Content::localized("command-wiki-success")))
716 .map_err(|e| {
717 Content::localized_with_args("command-wiki-fail", [(
718 "error",
719 LocalizationArg::from(e.to_string()),
720 )])
721 })
722}
723
724trait TabComplete {
729 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String>;
730}
731
732impl TabComplete for ArgumentSpec {
733 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
734 match self {
735 ArgumentSpec::PlayerName(_) => complete_player(part, client),
736 ArgumentSpec::EntityTarget(_) => {
737 if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) {
739 match spec {
740 "" => ClientEntityTarget::iter()
742 .filter_map(|target| {
743 let ident = target.keyword();
744 if ident.starts_with(end) {
745 Some(format!("@{ident}"))
746 } else {
747 None
748 }
749 })
750 .collect(),
751 "uid" => {
753 if let Some(end) =
755 u64::from_str(end).ok().or(end.is_empty().then_some(0))
756 {
757 client
759 .state()
760 .ecs()
761 .read_storage::<Uid>()
762 .join()
763 .filter_map(|uid| {
764 let uid = u64::from(*uid);
765 if end < uid {
766 Some(format!("uid@{uid}"))
767 } else {
768 None
769 }
770 })
771 .collect()
772 } else {
773 vec![]
774 }
775 },
776 _ => vec![],
777 }
778 } else {
779 complete_player(part, client)
780 }
781 },
782 ArgumentSpec::SiteName(_) => complete_site(part, client, i18n),
783 ArgumentSpec::Float(_, x, _) => {
784 if part.is_empty() {
785 vec![format!("{:.1}", x)] } else {
787 vec![] }
789 },
790 ArgumentSpec::Integer(_, x, _) => {
791 if part.is_empty() {
792 vec![format!("{}", x)]
793 } else {
794 vec![]
795 }
796 },
797 ArgumentSpec::Any(_, _) => vec![],
799 ArgumentSpec::Command(_) => complete_command(part, ""),
800 ArgumentSpec::Message(_) => complete_player(part, client),
801 ArgumentSpec::SubCommand => complete_command(part, ""),
802 ArgumentSpec::Enum(_, strings, _) => strings
803 .iter()
804 .filter(|string| string.starts_with(part)) .map(|c| c.to_string())
806 .collect(),
807 ArgumentSpec::AssetPath(_, prefix, paths, _) => {
809 if let Some(part_stripped) = part.strip_prefix('#') {
811 paths
812 .iter()
813 .filter(|string| string.contains(part_stripped))
814 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
815 .collect()
816 } else {
817 let part_with_prefix = prefix.to_string() + part;
819 let depth = part_with_prefix.split('.').count();
820 paths
821 .iter()
822 .map(|path| path.as_str().split('.').take(depth).join("."))
823 .dedup()
824 .filter(|string| string.starts_with(&part_with_prefix))
825 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
826 .collect()
827 }
828 },
829 ArgumentSpec::Boolean(_, part, _) => ["true", "false"]
830 .iter()
831 .filter(|string| string.starts_with(part))
832 .map(|c| c.to_string())
833 .collect(),
834 ArgumentSpec::Flag(part) => vec![part.to_string()],
835 }
836 }
837}
838
839fn complete_player(part: &str, client: &Client) -> Vec<String> {
841 client
842 .player_list()
843 .values()
844 .map(|player_info| &player_info.player_alias)
845 .filter(|alias| alias.starts_with(part))
846 .cloned()
847 .collect()
848}
849
850fn complete_site(mut part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
852 if let Some(p) = part.strip_prefix('"') {
853 part = p;
854 }
855 client
856 .sites()
857 .values()
858 .filter_map(|site| match site.marker.kind {
859 common_net::msg::world_msg::MarkerKind::Cave => None,
860 _ => Some(i18n.get_content(site.marker.name.as_ref()?)),
861 })
862 .filter(|name| name.starts_with(part))
863 .map(|name| {
864 if name.contains(' ') {
865 format!("\"{}\"", name)
866 } else {
867 name.clone()
868 }
869 })
870 .collect()
871}
872
873fn nth_word(line: &str, n: usize) -> Option<usize> {
875 let mut is_space = false;
876 let mut word_counter = 0;
877
878 for (i, c) in line.char_indices() {
879 match (is_space, c.is_whitespace()) {
880 (true, true) => {},
881 (true, false) => {
883 is_space = false;
884 word_counter += 1;
885 },
886 (false, true) => {
888 is_space = true;
889 },
890 (false, false) => {},
891 }
892
893 if word_counter == n {
894 return Some(i);
895 }
896 }
897
898 None
899}
900
901fn complete_command(part: &str, prefix: &str) -> Vec<String> {
904 ServerChatCommand::iter_with_keywords()
905 .map(|(kwd, _)| kwd)
906 .chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
907 .filter(|kwd| kwd.starts_with(part))
908 .map(|kwd| format!("{}{}", prefix, kwd))
909 .collect()
910}
911
912pub fn complete(line: &str, client: &Client, i18n: &Localization, cmd_prefix: &str) -> Vec<String> {
918 let word = if line.chars().last().is_none_or(char::is_whitespace) {
921 ""
922 } else {
923 line.split_whitespace().last().unwrap_or("")
924 };
925
926 if line.starts_with(cmd_prefix) {
928 let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
930 let mut iter = line.split_whitespace();
931
932 let cmd = iter.next().unwrap_or("");
934
935 let argument_position = iter.count() + usize::from(word.is_empty());
937
938 if argument_position == 0 {
940 let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
943 return complete_command(word, cmd_prefix);
944 }
945
946 let args = {
948 if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
949 Some(cmd.data().args)
950 } else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
951 Some(cmd.data().args)
952 } else {
953 None
954 }
955 };
956
957 if let Some(args) = args {
958 if let Some(arg) = args.get(argument_position - 1) {
960 arg.complete(word, client, i18n)
962 } else {
963 match args.last() {
965 Some(ArgumentSpec::SubCommand) => {
967 if let Some(index) = nth_word(line, args.len()) {
969 complete(&line[index..], client, i18n, "")
971 } else {
972 vec![]
973 }
974 },
975 Some(ArgumentSpec::Message(_)) => complete_player(word, client),
977 _ => vec![],
978 }
979 }
980 } else {
981 complete_player(word, client)
982 }
983 } else {
984 complete_player(word, client)
985 }
986}
987
988#[test]
989fn verify_cmd_list_sorted() {
990 let mut list = ClientChatCommand::iter()
991 .map(|c| c.keyword())
992 .collect::<Vec<_>>();
993
994 let list2 = list.clone();
996 list.sort_unstable();
997 assert_eq!(list, list2);
998}
999
1000#[test]
1001fn test_complete_command() {
1002 assert_eq!(complete_command("mu", "/"), vec!["/mute".to_string()]);
1003 assert_eq!(complete_command("unba", "/"), vec![
1004 "/unban".to_string(),
1005 "/unban_ip".to_string()
1006 ]);
1007 assert_eq!(complete_command("make_", "/"), vec![
1008 "/make_block".to_string(),
1009 "/make_npc".to_string(),
1010 "/make_sprite".to_string(),
1011 "/make_volume".to_string()
1012 ]);
1013}