1use std::{num::NonZeroU64, 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 Naga,
57 Unmute,
59 Waypoint,
62 Wiki,
64}
65
66impl ClientChatCommand {
67 pub fn data(&self) -> ChatCommandData {
73 use ArgumentSpec::*;
74 use Requirement::*;
75 let cmd = ChatCommandData::new;
76 match self {
77 ClientChatCommand::Clear => {
78 cmd(Vec::new(), Content::localized("command-clear-desc"), None)
79 },
80 ClientChatCommand::ExperimentalShader => cmd(
81 vec![Enum(
82 "Shader",
83 ExperimentalShader::iter()
84 .map(|item| item.to_string())
85 .collect(),
86 Optional,
87 )],
88 Content::localized("command-experimental_shader-desc"),
89 None,
90 ),
91 ClientChatCommand::Help => cmd(
92 vec![Command(Optional)],
93 Content::localized("command-help-desc"),
94 None,
95 ),
96 ClientChatCommand::Naga => cmd(vec![], Content::localized("command-naga-desc"), None),
97 ClientChatCommand::Mute => cmd(
98 vec![PlayerName(Required)],
99 Content::localized("command-mute-desc"),
100 None,
101 ),
102 ClientChatCommand::Unmute => cmd(
103 vec![PlayerName(Required)],
104 Content::localized("command-unmute-desc"),
105 None,
106 ),
107 ClientChatCommand::Waypoint => {
108 cmd(vec![], Content::localized("command-waypoint-desc"), None)
109 },
110 ClientChatCommand::Wiki => cmd(
111 vec![Any("topic", Optional)],
112 Content::localized("command-wiki-desc"),
113 None,
114 ),
115 }
116 }
117
118 pub fn keyword(&self) -> &'static str {
122 match self {
123 ClientChatCommand::Clear => "clear",
124 ClientChatCommand::ExperimentalShader => "experimental_shader",
125 ClientChatCommand::Help => "help",
126 ClientChatCommand::Naga => "naga",
127 ClientChatCommand::Mute => "mute",
128 ClientChatCommand::Unmute => "unmute",
129 ClientChatCommand::Waypoint => "waypoint",
130 ClientChatCommand::Wiki => "wiki",
131 }
132 }
133
134 pub fn help_content(&self) -> Content {
136 let data = self.data();
137
138 let usage = std::iter::once(format!("/{}", self.keyword()))
139 .chain(data.args.iter().map(|arg| arg.usage_string()))
140 .collect::<Vec<_>>()
141 .join(" ");
142
143 Content::localized_with_args("command-help-template", [
144 ("usage", Content::Plain(usage)),
145 ("description", data.description),
146 ])
147 }
148
149 pub fn iter() -> impl Iterator<Item = Self> + Clone {
151 <Self as strum::IntoEnumIterator>::iter()
152 }
153
154 pub fn iter_with_keywords() -> impl Iterator<Item = (&'static str, Self)> {
158 Self::iter().map(|c| (c.keyword(), c))
159 }
160}
161
162impl FromStr for ClientChatCommand {
163 type Err = ();
164
165 fn from_str(keyword: &str) -> Result<ClientChatCommand, ()> {
166 Self::iter()
167 .map(|c| (c.keyword(), c))
168 .find_map(|(kwd, command)| (kwd == keyword).then_some(command))
169 .ok_or(())
170 }
171}
172
173#[derive(Clone, Copy)]
179pub enum ChatCommandKind {
180 Client(ClientChatCommand),
181 Server(ServerChatCommand),
182}
183
184impl FromStr for ChatCommandKind {
185 type Err = ();
186
187 fn from_str(s: &str) -> Result<Self, ()> {
188 if let Ok(cmd) = s.parse::<ClientChatCommand>() {
189 Ok(ChatCommandKind::Client(cmd))
190 } else if let Ok(cmd) = s.parse::<ServerChatCommand>() {
191 Ok(ChatCommandKind::Server(cmd))
192 } else {
193 Err(())
194 }
195 }
196}
197
198type CommandResult = Result<Option<Content>, Content>;
207
208#[derive(EnumIter)]
213enum ClientEntityTarget {
214 Target,
216 Selected,
218 Viewpoint,
220 Mount,
222 Rider,
224 TargetSelf,
226}
227
228impl ClientEntityTarget {
229 const PREFIX: char = '@';
230
231 fn keyword(&self) -> &'static str {
232 match self {
233 ClientEntityTarget::Target => "target",
234 ClientEntityTarget::Selected => "selected",
235 ClientEntityTarget::Viewpoint => "viewpoint",
236 ClientEntityTarget::Mount => "mount",
237 ClientEntityTarget::Rider => "rider",
238 ClientEntityTarget::TargetSelf => "self",
239 }
240 }
241}
242
243fn preproccess_command(
249 session_state: &mut SessionState,
250 command: &ChatCommandKind,
251 args: &mut [String],
252) -> CommandResult {
253 let mut cmd_args = match command {
255 ChatCommandKind::Client(cmd) => cmd.data().args,
256 ChatCommandKind::Server(cmd) => cmd.data().args,
257 };
258 let client = &mut session_state.client.borrow_mut();
259 let ecs = client.state().ecs();
260 let player = ecs.read_resource::<PlayerEntity>().0;
261
262 let mut command_start = 0;
263
264 for (i, arg) in args.iter_mut().enumerate() {
265 let mut could_be_entity_target = false;
266
267 if let Some(post_cmd_args) = cmd_args.get(i - command_start..) {
268 for (j, arg_spec) in post_cmd_args.iter().enumerate() {
269 match arg_spec {
270 ArgumentSpec::EntityTarget(_) => could_be_entity_target = true,
271
272 ArgumentSpec::SubCommand => {
273 if let Some(sub_command) =
274 ServerChatCommand::iter().find(|cmd| cmd.keyword() == arg)
275 {
276 cmd_args = sub_command.data().args;
277 command_start = i + j + 1;
278 break;
279 }
280 },
281
282 ArgumentSpec::AssetPath(_, prefix, _, _) => {
283 *arg = prefix.to_string() + arg;
284 },
285 _ => {},
286 }
287
288 if matches!(arg_spec.requirement(), Requirement::Required) {
289 break;
290 }
291 }
292 } else if matches!(cmd_args.last(), Some(ArgumentSpec::SubCommand)) {
293 could_be_entity_target = true;
296 }
297 if could_be_entity_target && arg.starts_with(ClientEntityTarget::PREFIX) {
299 let target_str = arg.trim_start_matches(ClientEntityTarget::PREFIX);
301
302 let target = ClientEntityTarget::iter()
304 .find(|t| t.keyword() == target_str)
305 .ok_or_else(|| {
306 let expected_list = ClientEntityTarget::iter()
308 .map(|t| t.keyword().to_string())
309 .collect::<Vec<String>>()
310 .join("/");
311 Content::localized_with_args("command-preprocess-target-error", [
312 ("expected_list", LocalizationArg::from(expected_list)),
313 ("target", LocalizationArg::from(target_str)),
314 ])
315 })?;
316 let uid = match target {
317 ClientEntityTarget::Target => session_state
318 .target_entity
319 .and_then(|e| ecs.uid_from_entity(e))
320 .ok_or(Content::localized(
321 "command-preprocess-not-looking-at-valid-target",
322 ))?,
323 ClientEntityTarget::Selected => session_state
324 .selected_entity
325 .and_then(|(e, _)| ecs.uid_from_entity(e))
326 .ok_or(Content::localized(
327 "command-preprocess-not-selected-valid-target",
328 ))?,
329 ClientEntityTarget::Viewpoint => session_state
330 .viewpoint_entity
331 .and_then(|e| ecs.uid_from_entity(e))
332 .ok_or(Content::localized(
333 "command-preprocess-not-valid-viewpoint-entity",
334 ))?,
335 ClientEntityTarget::Mount => {
336 if let Some(player) = player {
337 ecs.read_storage::<Is<Rider>>()
338 .get(player)
339 .map(|is_rider| is_rider.mount)
340 .or(ecs.read_storage::<Is<VolumeRider>>().get(player).and_then(
341 |is_rider| match is_rider.pos.kind {
342 common::mounting::Volume::Terrain => None,
343 common::mounting::Volume::Entity(uid) => Some(uid),
344 },
345 ))
346 .ok_or(Content::localized(
347 "command-preprocess-not-riding-valid-entity",
348 ))?
349 } else {
350 return Err(Content::localized("command-preprocess-no-player-entity"));
351 }
352 },
353 ClientEntityTarget::Rider => {
354 if let Some(player) = player {
355 ecs.read_storage::<Is<Mount>>()
356 .get(player)
357 .map(|is_mount| is_mount.rider)
358 .ok_or(Content::localized("command-preprocess-not-valid-rider"))?
359 } else {
360 return Err(Content::localized("command-preprocess-no-player-entity"));
361 }
362 },
363 ClientEntityTarget::TargetSelf => player
364 .and_then(|e| ecs.uid_from_entity(e))
365 .ok_or(Content::localized("command-preprocess-no-player-entity"))?,
366 };
367
368 let uid = NonZeroU64::from(uid);
370 *arg = format!("uid@{uid}");
371 }
372 }
373
374 Ok(None)
375}
376
377pub fn run_command(
384 session_state: &mut SessionState,
385 global_state: &mut GlobalState,
386 cmd: &str,
387 mut args: Vec<String>,
388) -> CommandResult {
389 let command = ChatCommandKind::from_str(cmd)
390 .map_err(|_| invalid_command_message(&session_state.client.borrow(), cmd.to_string()))?;
391
392 preproccess_command(session_state, &command, &mut args)?;
393
394 match command {
395 ChatCommandKind::Server(cmd) => {
396 session_state
397 .client
398 .borrow_mut()
399 .send_command(cmd.keyword().into(), args);
400 Ok(None) },
403 ChatCommandKind::Client(cmd) => run_client_command(session_state, global_state, cmd, args),
404 }
405}
406
407fn invalid_command_message(client: &Client, user_entered_invalid_command: String) -> Content {
409 let entity_role = client
410 .state()
411 .read_storage::<Admin>()
412 .get(client.entity())
413 .map(|admin| admin.0);
414
415 let usable_commands = ServerChatCommand::iter()
416 .filter(|cmd| cmd.needs_role() <= entity_role)
417 .map(|cmd| cmd.keyword())
418 .chain(ClientChatCommand::iter().map(|cmd| cmd.keyword()));
419
420 let most_similar_cmd = usable_commands
421 .clone()
422 .min_by_key(|cmd| levenshtein(&user_entered_invalid_command, cmd))
423 .expect("At least one command exists.");
424
425 let commands_with_same_prefix = usable_commands
426 .filter(|cmd| cmd.starts_with(&user_entered_invalid_command) && cmd != &most_similar_cmd);
427
428 Content::localized_with_args("command-invalid-command-message", [
429 (
430 "invalid-command",
431 LocalizationArg::from(user_entered_invalid_command.clone()),
432 ),
433 (
434 "most-similar-command",
435 LocalizationArg::from(String::from("/") + most_similar_cmd),
436 ),
437 (
438 "commands-with-same-prefix",
439 LocalizationArg::from(
440 commands_with_same_prefix
441 .map(|cmd| format!("/{cmd}"))
442 .collect::<String>(),
443 ),
444 ),
445 ])
446}
447
448fn run_client_command(
453 session_state: &mut SessionState,
454 global_state: &mut GlobalState,
455 command: ClientChatCommand,
456 args: Vec<String>,
457) -> CommandResult {
458 let command = match command {
459 ClientChatCommand::Clear => handle_clear,
460 ClientChatCommand::ExperimentalShader => handle_experimental_shader,
461 ClientChatCommand::Help => handle_help,
462 ClientChatCommand::Naga => handle_naga,
463 ClientChatCommand::Mute => handle_mute,
464 ClientChatCommand::Unmute => handle_unmute,
465 ClientChatCommand::Waypoint => handle_waypoint,
466 ClientChatCommand::Wiki => handle_wiki,
467 };
468
469 command(session_state, global_state, args)
470}
471
472fn handle_clear(
474 session_state: &mut SessionState,
475 _global_state: &mut GlobalState,
476 _args: Vec<String>,
477) -> CommandResult {
478 session_state.hud.clear_chat();
479 Ok(None)
480}
481
482fn handle_experimental_shader(
484 _session_state: &mut SessionState,
485 global_state: &mut GlobalState,
486 args: Vec<String>,
487) -> CommandResult {
488 if args.is_empty() {
489 Ok(Some(Content::localized_with_args(
490 "command-experimental-shaders-list",
491 [(
492 "shader-list",
493 LocalizationArg::from(
494 ExperimentalShader::iter()
495 .map(|s| {
496 let is_active = global_state
497 .settings
498 .graphics
499 .render_mode
500 .experimental_shaders
501 .contains(&s);
502 format!("[{}] {}", if is_active { "x" } else { " " }, s)
503 })
504 .collect::<Vec<String>>()
505 .join("/"),
506 ),
507 )],
508 )))
509 } else if let Some(item) = parse_cmd_args!(args, String) {
510 if let Ok(shader) = ExperimentalShader::from_str(&item) {
511 let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
512 let res = if new_render_mode.experimental_shaders.remove(&shader) {
513 Ok(Some(Content::localized_with_args(
514 "command-experimental-shaders-disabled",
515 [("shader", LocalizationArg::from(item))],
516 )))
517 } else {
518 new_render_mode.experimental_shaders.insert(shader);
519 Ok(Some(Content::localized_with_args(
520 "command-experimental-shaders-enabled",
521 [("shader", LocalizationArg::from(item))],
522 )))
523 };
524
525 change_render_mode(
526 new_render_mode,
527 &mut global_state.window,
528 &mut global_state.settings,
529 );
530
531 res
532 } else {
533 Err(Content::localized_with_args(
534 "command-experimental-shaders-not-a-shader",
535 [("shader", LocalizationArg::from(item))],
536 ))
537 }
538 } else {
539 Err(Content::localized("command-experimental-shaders-not-valid"))
540 }
541}
542
543fn handle_help(
549 session_state: &mut SessionState,
550 global_state: &mut GlobalState,
551 args: Vec<String>,
552) -> CommandResult {
553 let i18n = global_state.i18n.read();
554
555 if let Some(cmd) = parse_cmd_args!(&args, ServerChatCommand) {
556 Ok(Some(cmd.help_content()))
557 } else if let Some(cmd) = parse_cmd_args!(&args, ClientChatCommand) {
558 Ok(Some(cmd.help_content()))
559 } else {
560 let client = &mut session_state.client.borrow_mut();
561
562 let entity_role = client
563 .state()
564 .read_storage::<Admin>()
565 .get(client.entity())
566 .map(|admin| admin.0);
567
568 let client_commands = ClientChatCommand::iter()
569 .map(|cmd| i18n.get_content(&cmd.help_content()))
570 .join("\n");
571
572 let server_commands = ServerChatCommand::iter()
574 .filter(|cmd| cmd.needs_role() <= entity_role)
575 .map(|cmd| i18n.get_content(&cmd.help_content()))
576 .join("\n");
577
578 let additional_shortcuts = ServerChatCommand::iter()
579 .filter(|cmd| cmd.needs_role() <= entity_role)
580 .filter_map(|cmd| cmd.short_keyword().map(|k| (k, cmd)))
581 .map(|(k, cmd)| format!("/{} => /{}", k, cmd.keyword()))
582 .join("\n");
583
584 Ok(Some(Content::localized_with_args("command-help-list", [
585 ("client-commands", LocalizationArg::from(client_commands)),
586 ("server-commands", LocalizationArg::from(server_commands)),
587 (
588 "additional-shortcuts",
589 LocalizationArg::from(additional_shortcuts),
590 ),
591 ])))
592 }
593}
594
595fn handle_naga(
599 _session_state: &mut SessionState,
600 global_state: &mut GlobalState,
601 _args: Vec<String>,
602) -> CommandResult {
603 let mut new_render_mode = global_state.settings.graphics.render_mode.clone();
604 new_render_mode.enable_naga ^= true;
605 let naga_enabled = new_render_mode.enable_naga;
606 change_render_mode(
607 new_render_mode,
608 &mut global_state.window,
609 &mut global_state.settings,
610 );
611
612 Ok(Some(Content::localized_with_args(
613 "command-shader-backend",
614 [(
615 "shader-backend",
616 if naga_enabled {
617 LocalizationArg::from("naga")
618 } else {
619 LocalizationArg::from("shaderc")
620 },
621 )],
622 )))
623}
624
625fn handle_mute(
627 session_state: &mut SessionState,
628 global_state: &mut GlobalState,
629 args: Vec<String>,
630) -> CommandResult {
631 if let Some(alias) = parse_cmd_args!(args, String) {
632 let client = &mut session_state.client.borrow_mut();
633
634 let target = client
635 .player_list()
636 .values()
637 .find(|p| p.player_alias == alias)
638 .ok_or_else(|| {
639 Content::localized_with_args("command-mute-no-player-found", [(
640 "player",
641 LocalizationArg::from(alias.clone()),
642 )])
643 })?;
644
645 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid))
646 && target.uuid == me.uuid
647 {
648 return Err(Content::localized("command-mute-cannot-mute-self"));
649 }
650
651 if global_state
652 .profile
653 .mutelist
654 .insert(target.uuid, alias.clone())
655 .is_none()
656 {
657 Ok(Some(Content::localized_with_args(
658 "command-mute-success",
659 [("player", LocalizationArg::from(alias))],
660 )))
661 } else {
662 Err(Content::localized_with_args(
663 "command-mute-already-muted",
664 [("player", LocalizationArg::from(alias))],
665 ))
666 }
667 } else {
668 Err(Content::localized("command-mute-no-player-specified"))
669 }
670}
671
672fn handle_unmute(
674 session_state: &mut SessionState,
675 global_state: &mut GlobalState,
676 args: Vec<String>,
677) -> CommandResult {
678 if let Some(alias) = parse_cmd_args!(args, String) {
681 if let Some(uuid) = global_state
682 .profile
683 .mutelist
684 .iter()
685 .find(|(_, v)| **v == alias)
686 .map(|(k, _)| *k)
687 {
688 let client = &mut session_state.client.borrow_mut();
689
690 if let Some(me) = client.uid().and_then(|uid| client.player_list().get(&uid))
691 && uuid == me.uuid
692 {
693 return Err(Content::localized("command-unmute-cannot-unmute-self"));
694 }
695
696 global_state.profile.mutelist.remove(&uuid);
697
698 Ok(Some(Content::localized_with_args(
699 "command-unmute-success",
700 [("player", LocalizationArg::from(alias))],
701 )))
702 } else {
703 Err(Content::localized_with_args(
704 "command-unmute-no-muted-player-found",
705 [("player", LocalizationArg::from(alias))],
706 ))
707 }
708 } else {
709 Err(Content::localized("command-unmute-no-player-specified"))
710 }
711}
712
713fn handle_waypoint(
715 session_state: &mut SessionState,
716 _global_state: &mut GlobalState,
717 _args: Vec<String>,
718) -> CommandResult {
719 let client = &mut session_state.client.borrow();
720
721 if let Some(waypoint) = client.waypoint() {
722 Ok(Some(Content::localized_with_args(
723 "command-waypoint-result",
724 [("waypoint", LocalizationArg::from(waypoint.clone()))],
725 )))
726 } else {
727 Err(Content::localized("command-waypoint-error"))
728 }
729}
730
731fn handle_wiki(
737 _session_state: &mut SessionState,
738 _global_state: &mut GlobalState,
739 args: Vec<String>,
740) -> CommandResult {
741 let url = if args.is_empty() {
742 "https://wiki.veloren.net/".to_string()
743 } else {
744 let query_string = args.join("+");
745
746 format!("https://wiki.veloren.net/w/index.php?search={query_string}")
747 };
748
749 open::that_detached(url)
750 .map(|_| Some(Content::localized("command-wiki-success")))
751 .map_err(|e| {
752 Content::localized_with_args("command-wiki-fail", [(
753 "error",
754 LocalizationArg::from(e.to_string()),
755 )])
756 })
757}
758
759trait TabComplete {
764 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String>;
765}
766
767impl TabComplete for ArgumentSpec {
768 fn complete(&self, part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
769 match self {
770 ArgumentSpec::PlayerName(_) => complete_player(part, client),
771 ArgumentSpec::EntityTarget(_) => {
772 if let Some((spec, end)) = part.split_once(ClientEntityTarget::PREFIX) {
774 match spec {
775 "" => ClientEntityTarget::iter()
777 .filter_map(|target| {
778 let ident = target.keyword();
779 if ident.starts_with(end) {
780 Some(format!("@{ident}"))
781 } else {
782 None
783 }
784 })
785 .collect(),
786 "uid" => {
788 let end_res = end.trim().parse().map_err(|_| Vec::<String>::new());
789 client
790 .state()
791 .ecs()
792 .read_storage::<Uid>()
793 .join()
794 .filter_map(|uid: &Uid| {
795 let u = uid.0;
796 match end_res {
797 Ok(e) if u > e => Some(format!("uid@{}", u.get())),
798 Ok(_) => None,
799 Err(_) => None,
800 }
801 })
802 .collect()
803 },
804 _ => vec![],
805 }
806 } else {
807 complete_player(part, client)
808 }
809 },
810 ArgumentSpec::SiteName(_) => complete_site(part, client, i18n),
811 ArgumentSpec::Float(_, x, _) => {
812 if part.is_empty() {
813 vec![format!("{:.1}", x)] } else {
815 vec![] }
817 },
818 ArgumentSpec::Integer(_, x, _) => {
819 if part.is_empty() {
820 vec![format!("{}", x)]
821 } else {
822 vec![]
823 }
824 },
825 ArgumentSpec::Any(_, _) => vec![],
827 ArgumentSpec::Command(_) => complete_command(part, ""),
828 ArgumentSpec::Message(_) => complete_player(part, client),
829 ArgumentSpec::SubCommand => complete_command(part, ""),
830 ArgumentSpec::Enum(_, strings, _) => strings
831 .iter()
832 .filter(|string| string.starts_with(part)) .map(|c| c.to_string())
834 .collect(),
835 ArgumentSpec::AssetPath(_, prefix, paths, _) => {
837 if let Some(part_stripped) = part.strip_prefix('#') {
839 paths
840 .iter()
841 .filter(|string| string.contains(part_stripped))
842 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
843 .collect()
844 } else {
845 let part_with_prefix = prefix.to_string() + part;
847 let depth = part_with_prefix.split('.').count();
848 paths
849 .iter()
850 .map(|path| path.as_str().split('.').take(depth).join("."))
851 .dedup()
852 .filter(|string| string.starts_with(&part_with_prefix))
853 .filter_map(|c| Some(c.strip_prefix(prefix)?.to_string()))
854 .collect()
855 }
856 },
857 ArgumentSpec::Boolean(_, part, _) => ["true", "false"]
858 .iter()
859 .filter(|string| string.starts_with(part))
860 .map(|c| c.to_string())
861 .collect(),
862 ArgumentSpec::Flag(part) => vec![part.to_string()],
863 }
864 }
865}
866
867fn complete_player(part: &str, client: &Client) -> Vec<String> {
869 client
870 .player_list()
871 .values()
872 .map(|player_info| &player_info.player_alias)
873 .filter(|alias| alias.starts_with(part))
874 .cloned()
875 .collect()
876}
877
878fn complete_site(mut part: &str, client: &Client, i18n: &Localization) -> Vec<String> {
880 if let Some(p) = part.strip_prefix('"') {
881 part = p;
882 }
883 client
884 .sites()
885 .values()
886 .filter_map(|site| match site.marker.kind {
887 common::map::MarkerKind::Cave => None,
888 _ => Some(i18n.get_content(site.marker.label.as_ref()?)),
890 })
891 .filter(|name| name.starts_with(part))
892 .map(|name| {
893 if name.contains(' ') {
894 format!("\"{}\"", name)
895 } else {
896 name.clone()
897 }
898 })
899 .collect()
900}
901
902fn nth_word(line: &str, n: usize) -> Option<usize> {
904 let mut is_space = false;
905 let mut word_counter = 0;
906
907 for (i, c) in line.char_indices() {
908 match (is_space, c.is_whitespace()) {
909 (true, true) => {},
910 (true, false) => {
912 is_space = false;
913 word_counter += 1;
914 },
915 (false, true) => {
917 is_space = true;
918 },
919 (false, false) => {},
920 }
921
922 if word_counter == n {
923 return Some(i);
924 }
925 }
926
927 None
928}
929
930fn complete_command(part: &str, prefix: &str) -> Vec<String> {
933 ServerChatCommand::iter_with_keywords()
934 .map(|(kwd, _)| kwd)
935 .chain(ClientChatCommand::iter_with_keywords().map(|(kwd, _)| kwd))
936 .filter(|kwd| kwd.starts_with(part))
937 .map(|kwd| format!("{}{}", prefix, kwd))
938 .collect()
939}
940
941pub fn complete(line: &str, client: &Client, i18n: &Localization, cmd_prefix: &str) -> Vec<String> {
947 let word = if line.chars().last().is_none_or(char::is_whitespace) {
950 ""
951 } else {
952 line.split_whitespace().last().unwrap_or("")
953 };
954
955 if line.starts_with(cmd_prefix) {
957 let line = line.strip_prefix(cmd_prefix).unwrap_or(line);
959 let mut iter = line.split_whitespace();
960
961 let cmd = iter.next().unwrap_or("");
963
964 let argument_position = iter.count() + usize::from(word.is_empty());
966
967 if argument_position == 0 {
969 let word = word.strip_prefix(cmd_prefix).unwrap_or(word);
972 return complete_command(word, cmd_prefix);
973 }
974
975 let args = {
977 if let Ok(cmd) = cmd.parse::<ServerChatCommand>() {
978 Some(cmd.data().args)
979 } else if let Ok(cmd) = cmd.parse::<ClientChatCommand>() {
980 Some(cmd.data().args)
981 } else {
982 None
983 }
984 };
985
986 if let Some(args) = args {
987 if let Some(arg) = args.get(argument_position - 1) {
989 arg.complete(word, client, i18n)
991 } else {
992 match args.last() {
994 Some(ArgumentSpec::SubCommand) => {
996 if let Some(index) = nth_word(line, args.len()) {
998 complete(&line[index..], client, i18n, "")
1000 } else {
1001 vec![]
1002 }
1003 },
1004 Some(ArgumentSpec::Message(_)) => complete_player(word, client),
1006 _ => vec![],
1007 }
1008 }
1009 } else {
1010 complete_player(word, client)
1011 }
1012 } else {
1013 complete_player(word, client)
1014 }
1015}
1016
1017#[test]
1018fn verify_cmd_list_sorted() {
1019 let mut list = ClientChatCommand::iter()
1020 .map(|c| c.keyword())
1021 .collect::<Vec<_>>();
1022
1023 let list2 = list.clone();
1025 list.sort_unstable();
1026 assert_eq!(list, list2);
1027}
1028
1029#[test]
1030fn test_complete_command() {
1031 assert_eq!(complete_command("mu", "/"), vec!["/mute".to_string()]);
1032 assert_eq!(complete_command("unba", "/"), vec![
1033 "/unban".to_string(),
1034 "/unban_ip".to_string()
1035 ]);
1036 assert_eq!(complete_command("make_", "/"), vec![
1037 "/make_block".to_string(),
1038 "/make_npc".to_string(),
1039 "/make_sprite".to_string(),
1040 "/make_volume".to_string()
1041 ]);
1042}